まくまくJavaノート
Comparable の実装を検討する
2016-09-07

Comparable インタフェースを実装する意味

java.lang.Comparable インタフェースを実装するクラスのインスタンスは、順序性を持った比較を行えることを意味します。

public interface Comparable<T> {
    int compareTo(T o);
}

Comparable オブジェクトのコレクションは、標準ライブラリとして用意されているユーティリティクラスを使ってソートできるようになります。

  • List 要素として格納した場合に Collections.sort でソートできる
  • 配列要素として格納した場合に Arrays.sort でソートできる

また、Comparable オブジェクトを前提としているソート済みコレクションの要素として扱うことができるようになります。

アルファベットや数値のように順序性を持っているとみなせる値クラスを定義する場合は、Comparable インタフェースを実装すべきです。

Comparable インタフェースの実装方法

Comparable#compareTo(T) は下記のように実装します。

  • 自分自身が渡されたオブジェクトより小さければ負の値を返す
  • 自分自身が渡されたオブジェクトより大きければ正の値を返す
  • 自分自身が渡されたオブジェクトと等しければ 0 を返す
  • 比較できないオブジェクトが渡された場合は ClassCastException を返す(適切にパラメータ化すればコンパイラが導入するブリッジメソッドによって自動的にこの実装が行われるので通常は意識する必要はない)

下記は Comparable インタフェースの実装例です。 double 型の浮動小数点数フィールドを比較する場合は、Double.compare を使用していることに注意してください。

CompositeNumber.java

public class CompositeNumber implements Comparable<CompositeNumber> {
    private int val1;
    private double val2;

    public CompositeNumber(int val1, double val2) {
        this.val1 = val1;
        this.val2 = val2;
    }

    @Override
    public int compareTo(CompositeNumber o) {
        if (val1 < o.val1) return -1;
        if (val1 > o.val1) return 1;
        return Double.compare(val2, o.val2);
    }
}

Comparable#compareTo(T) メソッドの契約である、「負の値を返す」「正の値を返す」という部分に注目してみてください。 これはつまり、負の値として -1、正の値として 1 といった固定の値を返す必要はないということを示しています。 この特性に気付いた人は、整数フィールドの比較は下記のように減算を利用して効率化すべきだと思うかもしれません。 ただ、この実装には Java 特有の問題が含まれています。

危険な実装?

@Override
public int compareTo(CompositeNumber o) {
    if (val1 != o.val1) return val1 - o.val1;  // オーバフローの可能性
    return Double.compare(val2, o.val2);
}

上記の val1 - o.val1 という減算処理では、オーバフローの可能性に留意する必要があります。 Java の int 型の減算処理では、論理的な計算結果が Integer.MAX_VALUE (2^31 - 1) を上回ってしまうとオーバフローが発生し、負の値が返されてしまいます。 下記のコードを実行してみれば分かりますが、正の値から負の値を引いた結果が負の値になってしまうケースがあります。

int a = 2000000000;
int b = -2000000000;
System.out.println(a - b);  // -294967296 !?

そのため、若干効率的ではなさそうに見えても、整数フィールドの比較には <> を用いた比較を行っておく方が安全です。 java.util.Date などの compareTo の実装も、下記のように比較演算子を使用して比較するようになっています。

public int compareTo(Date anotherDate) {
    long thisTime = getMillisOf(this);
    long anotherTime = getMillisOf(anotherDate);
    return (thisTime<anotherTime ? -1 : (thisTime==anotherTime ? 0 : 1));
}

Comparable なクラスを拡張する場合は継承ではなく委譲を使用する

Comparable インタフェースの契約として、推移的であること というものがあります。

(x.compareTo(y)>0 && y.compareTo(z)>0) implies x.compareTo(z)>0.

また、対称性があることも求められています。

x.compareTo(y) の符号 != y.compareTo(x) の符号

Comparable を実装したクラスを継承して、compareTo メソッドをオーバライドしてしまうと、スーパークラスのインスタンスと、サブクラスのインスタンスを比較する際に、すべての契約を満たすことができないケースが出てきてしまいます(複雑なので詳細は省略)。 つまり、Comparable を実装している値クラスは継承してはいけない、という結論に達します。 値クラスを拡張する場合は、新しいクラスで、拡張したい値クラスのオブジェクトを保持する形(委譲)で拡張するようにしましょう。 新しいクラスで Comparable を実装する場合は、保持している値クラスの compareTo を利用して実装すれば OK です。

2016-09-07