Edited at

[Java] Comparable, Comparator のメモ


目的


  • 大小比較をする java.util.Comparatorjava.lang.Comparable に関するクラス・メソッドの整理


  • ComparatorComparable を実装する場合の注意点を整理


基本的な話


Comparable , Comparator 共通


  • いわゆる「宇宙船演算子」と同じようなもの

  • 大小比較する

  • ソート、バイナリサーチする場合、max/min などを求める場合、ツリー構造( TreeSet , TreeMap )を作る際などにも使われる

  • 基本的に equals() と矛盾しないことが推奨される(もし equals() と互換性がない場合には、その旨をjavadocに明記すること)

  • 基本的に Serializable を実装することが推奨される


Comparable とは


  • 自分自身と、別のインスタンスの大小比較をする


  • Comparable を実装していると「自然な順序付け」ができる


Comparator とは


  • 2 つのオブジェクトの大小比較をする


便利なメソッド


  • java.util.Collections#reverseOrder()

引数をとるものと、引数なしのものがあります。

引数をとるものは、指定された Comparator の逆順の Comparator を返します。

引数をとらないものは、自然な順序の逆順となる Comparator を返します。

        // 自然な順序の逆順

Comparator<String> c1 = Collections.reverseOrder();
// 指定された Comparator の逆順
Comparator<String> c2 = Collections.reverseOrder(c1);

Java 8 から、同じ目的のメソッドが Comparator にも追加されました。


  • java.util.Comparator#reverseOrder()

前述の引数なしの Collections#reverseOrder() と同じ、自然な順序の逆順となる Comparator を返します。

Comparator 系のメソッドを、Comparator インタフェースに集約したものだと思います。

        // 自然な順序の逆順

Comparator<String> c3= Comparator.reverseOrder();


  • java.util.Comparator#reversed()

前述の引数ありの Collections#reverseOrder() のように逆順の Comparator を返します。違いは、引数で渡されたインスタンスを対象とするのではなく、インスタンスそのもののメソッドであることです。

        // 自分の逆順

Comparator<String> c4 = c3.reversed();


  • java.util.Comparator#naturalOrder()

自然な順序の Comparator を取得できます。


  • java.util.Comparator#comparing()

オブジェクトから値を取り出して比較する Comparator を生成します。

ラムダあるいはメソッド参照で指定するのが一般的だと思います。

public class Bean {

private String data1;

public String getData1() {
return data1;
}
}

// ...

Comparator<Bean> c5 = Comparator.comparing(Bean::getData1);

プリミティブ版の comparingInt(), comparingLong(), comparingDouble() などもあります。


  • java.util.Comparator#thenComparing()

Comparator を結合して、1 つ目の Comparator の結果が 0 だった場合(等しい場合)に、2 つ目の Comparator の結果を使います。

3 つ以上を結合することもできます。

public class Bean {

private String data1;
private String data2;

public String getData1() {
return data1;
}

public String getData2() {
return data2;
}
}

// ...

Comparator<Bean> c6 = Comparator.comparing(Bean::getData1)
.thenComparing(Bean::getData2);



  • java.util.Comparator#nullsFirst() , nullsLast()

引数で渡した Comparatornull セーフにします。2 つのメソッドの違いは null の項目を null 以外の項目の前に持ってくるか( nullsFirst )、後にもってくるか( nullsLast )です。


  • java.lang.String#CASE_INSENSITIVE_ORDER

String クラスには、英大小文字を無視した大小比較用の Comparator が定数として用意されています。


Map.Entry の比較

Java 8 から Map.Entry の Key あるいは Value で比較することが簡単にできるようになっています。おそらく Stream などでの使用を想定しているのかと思います。


  • java.util.Map.Entry#comparingByKey()

  • java.util.Map.Entry#comparingByKey(Comparator<? super K>)

  • java.util.Map.Entry#comparingByValue()

  • java.util.Map.Entry#comparingByValue(Comparator<? super K>)


  • java.util.Objects#compare()


Objects クラスには、オブジェクトの大小チェックができる compare() メソッドがあります。Comparable を実装しているオブジェクトの比較が可能です。

独自クラスの ComparableComparator を実装する際、メンバー変数の比較をするときなどに便利です。

また、ラムダ式で Comparator を作るときにも便利です。



  • java.lang.Integer#compare() , java.lang.Long#compare() など

類似のメソッドは Boolean, Byte, Character, Short, Integer, Long, Float, Double などのプリミティブ型のラッパークラスにもあります。


注意点


equals() と矛盾しないこと

ComparableComparator の実装では、必ずしも equals() と矛盾しない結果を出す必要はありません。

実際に標準ライブラリには、Comparable を実装していても equals() と互換性がない結果を返すものがあります(たとえば java.math.BigDecimal )。

しかし基本は equals() と矛盾しない結果を返すことが推奨されます。

これは TreeSetTreeMap などいくつかの標準ライブラリが、Comparable (や Comparator)が 0 を返すことと、equals()true を返すことを同一視して処理しているためです。

たとえば Comparable を実装するクラスが、メンバ変数として data1, data2, data3 を持っており、equals() がその 3 つのメンバで判定しているとします。

その場合には(たとえ、ソートする条件としては data1, data2 だけでも十分だったとしても)、compareTo() でも同じく 3 つのメンバ変数で判定する必要があるということです。

そうしないと data1, data2 が同じで data3 が違うものを、キー重複と判断し片方が捨てられてしまうことがあります。

public final class Bean implements Comparable<Bean>, Serializable {

private static final long serialVersionUID = 1;

private String data1;
private String data2;
private String data3;

@Override
public int hashCode() {
return Objects.hash(data1, data2, data3);
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof Bean) {
Bean that = (Bean) other;
return Objects.equals(this.data1, that.data1)
&& Objects.equals(this.data2, that.data2)
&& Objects.equals(this.data3, that.data3);
}
return false;
}

@Override
public int compareTo(Bean that) {
Comparator<String> naturalOrder = Comparator.naturalOrder();
int rc = Objects.compare(data1, that.data1, naturalOrder);
if (rc == 0) {
rc = Objects.compare(data2, that.data2, naturalOrder);
if (rc == 0) {
rc = Objects.compare(data3, that.data3, naturalOrder);
}
}
return rc;
}
}


シリアライズ可能とすること

Comparator の実装では、同様に基本はシリアライズ可能にすることが推奨されています。

TreeMapTreeSet などに Comparator を設定したあとに、シリアライズをされるようなことがあっても問題ないようにするためです。

標準ライブラリが生成する Comparator は、もちろんシリアライズ可能になっています。

ただし気をつけなくてはならないのは、普通のラムダ式やメソッド参照などは(交差型キャストをしなければ)シリアライズ可能ではないということです。

public final class Bean implements Comparable<Bean>, Serializable {

private static final long serialVersionUID = 1;
private static final Comparator<Bean> COMPARATOR = Comparator.comparing(Bean::getData1)
.thenComparing(Bean::getData2)
.thenComparing(Bean::getData3);

private String data1;
private String data2;
private String data3;

public String getData1() {
return data1;
}

public String getData2() {
return data2;
}

public String getData3() {
return data3;
}

@Override
public int compareTo(Bean that) {
return COMPARATOR.compare(this, that);
}
}

たとえば上記のように生成した COMPARATOR はシリアライズ不可能です。

もし、これを public にして外部から使ってもらった場合、一見問題ないように見えますが、その COMPARATOR をシリアライズしようとしたら例外が発生してしまいます。

この例で言えば COMPARATORprivate のままにしておいて、Comparator#naturalOrder() で処理してもらうほうが安全だと思います。

もちろんデータをソートする、大小チェックするなど、その場で操作が完了するような状況で利用するにはシリアライズ不可能なラムダ式でも問題ありません。


継承に気をつけること

継承による親子関係があると、equals() や大小比較が非常に複雑になります(特に対称性の確保について)。

通常、データを入れるだけの Bean (DTO など) は継承して項目を追加などは不要なことがほとんどだと思います。

その場合、親クラスを java.lang.Object として、final クラスとして定義することで、そういった難しい問題を確実に避けることができます。


その他


比較結果の見かた

ComparableComparator の戻り値は、正零負の3種類があります。

覚え方は何種類かありますが、以下のように覚えるのが簡単かと思います。

結果
比較
意味


compare(a, b) > 0
a > b

非負
compare(a, b) >= 0
a >= b


compare(a, b) ==0
a == b

非正
compare(a, b) <= 0
a <= b


compare(a, b) < 0
a < b


  • ゼロと比較する

  • ゼロは比較演算子の右側に置く


  • compare() と 0 の間の比較演算子は、a と b の間においたものと考える


愚痴



  • Comparator#comparingBoolean() がないのがちょっと面倒。ラムダ式で書けばいいのだけど。

  • nullセーフ+自然順序や、nullセーフ名+comparing() など頻発する。毎回書くのは煩雑な気がする

  • ラムダは全部、Serializeable にしておけば良いのに…。