読者です 読者をやめる 読者になる 読者になる

【Effective Java】項目12:Comparable の実装を検討する

Java Effective Java

Comparable インタフェースの compareTo を実装するとインスタンスが順序を持つようになる。

第3章で議論した他のメソッドとは異なり、compareTo メソッドは Object では宣言されていない。 ただし、Comparable を実装すると、多くの一般的なアルゴリズムやコレクション実装と一緒にクラスを利用することができるようになる。

Java ライブラリの値クラスのほとんどが Comparable を実装している。 アルファベット順、数値順、年代順など、自然な順序を持つ値クラスを実装する場合は、このインターフェスを実装することを検討するべきである。

compareTo の一般契約

compareTo の一般契約は equals のそれと似ている。 compareTo は指定されたオブジェクトと自分自身の順序を比較する。 オブジェクトが自身と比べて、小さい、等しい、大きい、に応じて、負の整数、ゼロ、正の整数を返す。

表記 sgn(EXPRESSION) は数学上の符号関数を意味し、負、ゼロ、正のどれであるかに応じて -1, 0, 1 を返すとすると、compareTo の一般契約は以下の通り。

  • すべての x と y に関して sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) を保証する
    • y.compareTo(x) が例外をスローする場合にのみ、x.compareTo(y) は例外をスローしなければならない
  • すべての x と y に関して 0 < x.compareTo(y) かつ 0 < y.compareTo(z) ならば 0 < x.compareTo(z) であることを保証する(推移性)。
  • すべての z に関して、x.compareTo(y) == 0 が sgn(x.compareTo(z)) == sgn(y.compareTo(z)) を保証する
    • 等しい順序とされたオブジェクトは、他のオブジェクト z と比較した場合にも同じ符号を持つ
  • (x.compareTo(y) == 0) == (x.equals(y)) は強く推奨されるが、厳密には必須ではない
    • ただし、この条件を破る場合はそのことを明記する

もし、指定されたオブジェクトが比較できない場合は ClassCastException をスローする。 一般契約のレベルでは、他のクラスとの順序比較を排除していないが、Java ライブラリにクラス間の比較をサポートするものはない。

equals との関係

compareTo の一般契約は equals のものと似ており、反射性、対称性、推移性に従う必要がある。

そのため、equals と同様に、オブジェクトの抽象化の恩恵を諦めずに、インスタンス化可能な値クラスを拡張して、これらの契約を守ったまま値を追加する方法はない (項目8)。この場合には「ビュー」メソッドを提供するべきである。

compareTo と equals が一致しているとき、「順序が equals と一致している」と言われる。 逆の場合、「順序が equals と矛盾している」と言われる。 equals と矛盾する compareTo 自体は機能するが、Java のコレクションインタフェースの契約には従わないかもしれない。

例えば、equals と矛盾した compareTo を持っている BigDecimal クラスを考える。 new BigDecimal("1.0") と new BigDecimaal("1.00") を HashSet に追加すると、そのセットは2つの要素を保持する。 なぜなら、これらは equals を使用して比較され、その場合は等しくないと判定されるからである。

一方、同じ処理を TreeSet で行うと、そのセットは要素を一つだけ保持する。 なぜなら、TreeSet においては compareTo メソッドを利用して、値が比較されるからである。

compareTo の実装

compareTo の実装では、基本データ型は < や > を使って比較し、参照型は compareTo で再帰的に比較する。 浮動小数点に関しては Double.compare か Float.comapre を利用する。 クラスがフィールドを複数持っている場合、最も強い意味のあるフィールドから比較を始めること。

例えば、項目9の PhoneNumber の compareTo は以下の通りです。

@Override
public int compareTo(PhoneNumber o) {
    if (areaCode < o.areaCode) return -1;
    if (o.areaCode < areaCode) return  1;

    if (prefix < o.prefix) return -1;
    if (o.prefix < prefix) return 1;

    if (lineNumber < o.lineNumber) return -1;
    if (o.lineNumber < lineNumber) return 1;

    return 0;
}

もし、返り値として符号だけが必要なことが分かっていれば、以下の様に改善できる。

@Override
public int compareTo(PhoneNumber o) {
    int areaCodeDiff = areaCode - o.areaCode;
    if (areaCodeDiff != 0) return areaCodeDiff;

   int prefixDiff = prefix - o.prefix;
   if (prefixDiff != 0)  return prefixDiff;

   return lineNumber - o.lineNumber;
}

ただし、このコードはオーバーフローした場合に、意味のない値を返すことがありえるため注意が必要である。

感想

Comparable と Comparator ってややこしいよな。

Comparable を手元で実装しようとして Comparator を実装してしまって、しかもメソッド名が compare で compareTo に似てて余計混乱した。 「あれ?API 変わったのかな?」みたいなね。

みなさんも気をつけましょう。 普通、間違えないけどな ァ ’`,、’`,、('∀`) ’`,、’`,、

そういえば、気がついたら、第3章終わってた。 このペースだと11月末くらいまでかかりそうだなぁ、、、。