[Java] Comparable<T>問題

2018年10月22日月曜日

Java ジェネリクス

Javaで区間を表すクラスが欲しかったけど、ジェネリック型の取り扱いで右往左往してしまった。

最初のアイデア

Comparableを2つ持つabstractクラスを作って、実際の比較対象の型は継承先のクラスで決めればいいと思ったので、以下のようなRangeクラスを作った。

Range.java
abstract class Range<E, T extends Range<E, T>> {
    private final Comparable<E> begin;
    private final Comparable<E> end;

    protected Range(Comparable<E> begin, Comparable<E> end) {
        this.begin = begin;
        this.end = end;
    }

    public Comparable<E> getBegin() { return begin; }
    public Comparable<E> getEnd() { return end; }
}
LongRange.java
class LongRange extends Range<Long, LongRange> {
    LongRange(Long begin, Long end) {
        super(begin, end);
    }
}

とりあえず、コンパイルも通り、問題なさそう。
ところが、beginとendを比較するメソッドを作ったところで問題発生。

Comparable<T>

2つのRange同士がの期間が重なっているかどうかを比較するメソッドを作ったところ、コンパイルエラーになった。

Range.javaに以下のメソッドを追加。
    boolean overlap(T t) {
        return getBegin().compareTo(t.getEnd()) <= 0 &&
             t.getBegin().compareTo(getEnd())   <= 0;
    }
エラー: 不適合な型: Comparable<E>をEに変換できません:
        return getBegin().compareTo(t.getEnd()) <= 0 &&
                                            ^
  Eが型変数の場合:
    クラス Rangeで宣言されているEはObjectを拡張します

Comparable.compareTo(T o)の引数はTなのに、Comparable<T>を渡そうとしているのが問題。

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/Comparable.html#compareTo-T-

これは完全に勘違い。
この場合、beginとendの型をComparableインターフェースにするのではなく、上限境界型で表す必要がある。
修正後のRangeクラスはこう。

Range.java
abstract class Range<E extends Comparable<E>, T extends Range<E, T>> {
    private final E begin;
    private final E end;

    protected Range(E begin, E end) {
        this.begin = begin;
        this.end = end;
    }

    public E getBegin() { return begin; }
    public E getEnd() { return end; }

    boolean overlap(T t) {
        return getBegin().compareTo(t.getEnd()) <= 0 &&
             t.getBegin().compareTo(getEnd())   <= 0;
    }
}

これで、一見問題なさそうだけど、新しいサブクラスを作ったら別の問題発覚。

java.time.LocalDate

区間といえば、日付型。ということで、LocalDateを使った日付用のサブクラスを作る。

DateRange.java
import java.time.LocalDate;

class DateRange extends Range<LocalDate, DateRange> {
    DateRange(LocalDate begin, LocalDate end) {
        super(begin, end);
    }
}

これでコンパイルすると、以下のエラーが出る。
エラー: 型引数LocalDateは型変数Eの境界内にありません
    static class DateRange extends Range<LocalDate, DateRange> {
                                         ^
  Eが型変数の場合:
    クラス Rangeで宣言されているEはComparable<E>を拡張します

で、結局こうなった。

これは LocalDate が Comparable<LocalDate> ではなく、Comparable<ChronoLocalDate> であることが原因。

https://docs.oracle.com/javase/jp/8/docs/api/java/time/LocalDate.html

Comparableインターフェースのデザイン上、Comparableを実装したクラスをさらに継承すると、このようになってしまう。
こういうケースでは、下限境界で型を表す必要がある。
abstract class Range<E extends Comparable<E>, T extends Range<E, T>> {
を以下にする。
abstract class Range<E extends Comparable<? super E>, T extends Range<E, T>> {

最終的なRangeクラスは以下の通り。

Range.java
abstract class Range<E extends Comparable<? super E>, T extends Range<E, T>> {
    private final E begin;
    private final E end;

    protected Range(E begin, E end) {
        this.begin = begin;
        this.end = end;
    }

    public E getBegin() { return begin; }
    public E getEnd() { return end; }

    boolean overlap(T t) {
        return getBegin().compareTo(t.getEnd()) <= 0 &&
             t.getBegin().compareTo(getEnd())   <= 0;
    }
}