Comparableを実装するメリットと実装する場合に守るべき内容について記載しています。
記載内容
自然な順序を持つことのメリット
クラスがComparableを実装することで、そのクラスのインスタンスが自然な順序を持っていることを示せる。
文字列における辞書順、数値の大小関係等、明らかな順序を持つクラスであれば、実装を検討するべきである。
Comparableを実装するわずかな努力で、コレクションや配列のソート、TreeMapなどの順序付けに依存したAPIを使用等、多くのメリットを得ることができる。
Comparableの一般契約
ComparableのcompareToメソッドの一般契約は、equalsメソッドの一般契約に似ている。
(以降、x yの順序について、x が yより順序が先の場合、x < y 、同じ順序の場合 x = y と表現する)
equalsと同様、インスタンス化可能なクラスを拡張して、equals契約を守ったまま値要素を追加する方法はない。
対称性
x に対して y を比較した場合と、y に対して x を比較した場合の順序について、整合性がとれている必要がある。
つまり
- x < y なら、y > x
- x = y なら、y = x
- x.compareTo(y)で例外が出る場合、y.compareTo(x)でも例外が出る
推移性
x < y、y < zであれば、x < z となる。
比較に対する一貫性
x = y かつ x < z なら、y < z
equalsメソッドとの一貫性
x = y と x.equals(y)==true の合致は強く推奨されますが、必須ではない。
標準クラスではBigDecimalがequalsとcompareToが合致していない。
1.00と1.0は、equalsでは合致しないが、compareToでは合致する。
このようなクラスはその旨をJavaDocで記載するべきである。
実装方法
Java8から追加されたComparatorクラスのcomparing、thenComparing等を使って、
以下の様にメソッド呼び出しをチェーンさせて比較処理を構築することができる。
class A implements Comparable<A> {
・・・
private static final Comparator<A> COMPARATOR =
Comparator.comparing(A::getName)
.thenComparing(A::getAge);
@Override
public int compareTo(A other) {
return COMPARATOR.compare(this, other);
}
}
考察
equals同様に長い項目ですが、Comparableに関する基本的な内容であり、
特に一般契約については理解しておく必要が有ります。
一般契約への補足
JavaDoc、EffectiveJavaとも明確な記載はありませんが、
反射性も満たす必要があります。
これは対称性に対するJavaDoc上の記載
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
の y を x に置き換えることで導き出されます。
既存のクラスへの実装の追加
equalsと異なり、既存のクラスに対してComparableの実装を追加するのは問題ありません。
知らない間にcompareToを使用してしまっていることは無いためです。
(instanceofまで考えると影響する可能性はありますが、通常は問題が発生しないはずです)
そのため、順序付けが必要になったら実装を追加する、という指針でも大きな問題にはならないかと思います。
Comparableを実装したクラスの継承と委譲による問題の回避
Comparableを実装したクラスの継承は、equalsで発生する問題よりも状況が悪くなります。
ジェネリクスに関する制約のため、以下の様な継承を行うとコンパイルエラーになります。
class A implements Comparable<A> {
・・・
}
class B extends A implements Comparable<B> {
・・・
}
ジェネリクスが導入されるまではcompareToの引数はObjectでしたのでequalsと同等の状況で、
子クラスでcompareToをオーバライドし、子クラス特有の比較を実装することができましたが、
これもできません。
委譲であればこの問題に対処できますので、やはり継承よりも委譲を選択するべきです。