目的
- 大小比較をする
java.util.Comparator
とjava.lang.Comparable
に関するクラス・メソッドの整理 -
Comparator
やComparable
を実装する場合の注意点を整理
基本的な話
Comparable
, Comparator
共通
- いわゆる「宇宙船演算子」
<=>
と同じようなもの→ Wikipedia: 宇宙船演算子 - 大小比較する
- ソート、バイナリサーチする場合、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()
引数で渡した Comparator
を null
セーフにします。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
を実装しているオブジェクトの比較が可能です。
独自クラスの Comparable
や Comparator
を実装する際、メンバー変数の比較をするときなどに便利です。
また、ラムダ式で Comparator
を作るときにも便利です。
-
java.lang.Integer#compare()
,java.lang.Long#compare()
など
類似のメソッドは Boolean
, Byte
, Character
, Short
, Integer
, Long
, Float
, Double
などのプリミティブ型のラッパークラスにもあります。
注意点
equals()
と矛盾しないこと
Comparable
や Comparator
の実装では、必ずしも equals()
と矛盾しない結果を出す必要はありません。
実際に標準ライブラリには、Comparable
を実装していても equals()
と互換性がない結果を返すものがあります(たとえば java.math.BigDecimal
)。
しかし基本は equals()
と矛盾しない結果を返すことが推奨されます。
これは TreeSet
や TreeMap
などいくつかの標準ライブラリが、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
の実装では、同様に基本はシリアライズ可能にすることが推奨されています。
TreeMap
や TreeSet
などに 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
をシリアライズしようとしたら例外が発生してしまいます。
この例で言えば COMPARATOR
は private
のままにしておいて、Comparator#naturalOrder()
で処理してもらうほうが安全だと思います。
もちろんデータをソートする、大小チェックするなど、その場で操作が完了するような状況で利用するにはシリアライズ不可能なラムダ式でも問題ありません。
継承に気をつけること
継承による親子関係があると、equals()
や大小比較が非常に複雑になります(特に対称性の確保について)。
通常、データを入れるだけの Bean (DTO など) は継承して項目を追加などは不要なことがほとんどだと思います。
その場合、親クラスを java.lang.Object
として、final
クラスとして定義することで、そういった難しい問題を確実に避けることができます。
その他
比較結果の見かた
Comparable
や Comparator
の戻り値は、正零負の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 にしておけば良いのに…。