Genericsとか

Java : Generics - lethevert is a programmer

あたりを読んで、Genericsって難しいって思う人がたくさんいるんだなと。

確かに難しいけど、それは

とかその辺の話で、末端の開発者(上の成果を利用する人)はほとんど気にせずに使えると思うのですよ。Javaの仕様が複雑、と言っているほとんどの人は上に関わっている人とか、言語学者?の人なのかなと思います。

たとえば、enumの親であるjava.lang.Enumジェネリッククラスとして宣言されていて、

class Enum<E extends Enum<E>>

上のような宣言ですが、これを気にする人とかあまりいないのではないかと思います。
それでも、これらを利用する開発者にとっては気がつかないうちにいろいろな恩恵が与えられています。

ほかにも、java.util.Collections.sortなんかは

public static <T> void sort(List<T> list,
                            Comparator<? super T> c)

こんな感じのシグネチャもってますが、? super T って言うのを完全に理解しなくても

List<String> list = ...;
Comparator<String> cmp = ...;
Collections.sort(list, cmp);

って書けます。

これがもし、ワイルドカード指定できなかったりすると、こんな感じで非常に範囲が厳しくなったり、

// 型の固定
public static <T> void sort(List<T> list, Comparator<T> c)

...
List<String> list = ...;
Comparator<CharSequence> cmp = ...;
Collections.sort(list, cmp); // コンパイルエラー

こんな感じで型安全じゃなくなったりするわけで。

// ジェネリックス使わない
public static void sort(List list, Comparator c)

...
List<String> list = ...;
Comparator<Integer> cmp = ...;
Collections.sort(list, cmp); // 実行時エラー

もちろん、Genericsの導入によってJavadocが読みにくくなったことは否定しませんが、便利になった部分とのトレードオフが難しいところでしょうか。

結局、Javaジェネリクスは次の2つの機能しか提供しません。

  1. 型安全のためのコンパイラへのヒント
  2. キャストが不要になる

利用側からはほとんど気にすることなくジェネリクスの恩恵にあずかれますが、提供側はそれなりに注意しなければなりません。具体的には、下記の2点を考慮することは必須だと思います。

  1. ジェネリクスの導入によって型安全になる
    • 異常系を考慮しなくても、静的型推論で何とかしてくれる
    • ヒープ汚染が発生するのは論外
  2. 同じAPIをイレイジャで提供したときと比較し、正常系の機能が落ちない
    • Collections.sortの例のように、適切な範囲の型を指定する

提供側にとって複雑であることはどうでもよく、利用側にとって単純であることだけが重要だと思います。そういう意味では、ジェネリクスというシステム自体は複雑だと思います。ただ、それをもって「ジェネリクスは複雑だ」と単純に決め付けてしまうのもどうかと思います。

複雑な例として、下のメソッドがあります。これは「同じAPIをイレイジャで提供したときと比較し、正常系の機能が落ちない」というルールに対して違反しています。

/**
 * 2つのリストを結合し、さらに比較器を用いてソートした新しいリストを返す。
 * @param <T> ソートする対象の型
 * @param a 結果に含まれるリスト
 * @param b 結果に含まれるリスト
 * @param c 比較器
 * @return 2つのリストを結合し、さらに比較器を用いてソートした新しいリスト
 * @throws NullPointerException 引数に{@code null}が指定された場合
 */
public static <T> List<T> sort(List<T> a, List<T> b, Comparator<T> c) {
    List<T> result = new ArrayList<T>(a.size() + b.size());
    result.addAll(a);
    result.addAll(b);
    Collections.sort(result, c);
    return result;
}

これを考えるには、とりあえず全部別の型変数においてみます。

<R, A, B, C> List<R> sort(List<A> a, List<B> b, Comparator<C> c)

それで、R, A, B, Cそれぞれの関係を考えて見ます。

R :> A (結果は元のリストの要素を含むので、RはAの親でよい)
R :> B (同上)
R <: C (結果は比較されたもののリストなので、CはRの親でよい)
A <: C (元のリストは比較されるので、CはAの親でよい)
B <: C (同上)
なお、AとBは特別な関係がなくてもよい

これを数直線上に表すと、次のような2本の数直線ができます。

A <: R <: C
B <: R <: C

そうすると、Rを中心に次のような関係になります。

<C, R extends C, A extends R, B extends R> List<R> sort(List<A> a, List<B> b, Comparator<C> c)

ここで、最終的にAPI利用者が実際に利用する型を考えると、戻り値のリストに含まれるRのみで、ほかはそれとの親子関係だけで十分です。

A -> ? extends R (A <: R)
B -> ? extends R (B <: R)
C -> ? super R (R <: C)

ついでに、RをTに名前変更すると、次のようなシグネチャになります。

public static <T> List<T> sort(List<? extends T> a, List<? extends T> b, Comparator<? super T> c);

APIとしてはいまいちですが、こんな使い方ができます。

List<Integer> a = ...;
List<Double> b = ...;
Comparator<Object> c = ...;
// sort(a, b, c) だと List<? extends Number> になってしまう、実は利用者に複雑な例orz
List<Number> result = Main.<Number>sort(a, b, c);

慣れれば脳内でこんなことができますが、そうでないうちは型の関係性をいちいち考える必要があります。複雑だと思ううちは、できるだけJavaの標準APIで使われているジェネリクスをまねするのがいいかなと思います。

うーん。話題がぶれた。