Javaのジェネリックは1.4以前のバイナリ仕様との互換のために、コンパイルの時点で型引数情報が消去され非ジェネリックプログラミングされたかのようなコードに変換されます。この変換を 型消去(イレイジャ) と呼びます。
型消去、通常のプログラミングにもたまに影響を与えます(例えば List.toArray() がなぜか引数として一見不要な E[] 型の配列オブジェクトを要求するなどします)が、何より影響を受けるのがリフレクションごりごりのフレームワークプログラミングやライブラリプログラミングにおいてです。型消去に伴って、実行時型情報からも型引数の部分が抜け落ちてしまいますから(そこだけは残しておいてくれれば良かったのに!)。
具体的にはどんなことになるかというと、たとえばみんな大好きEBean(うそ、僕が大好き)なら、エンティティクラスを指定して対応するDBテーブルからデータを取り出すのに
List<User> result = Ebean.<User>find(User.class).findList();
なんて記述の仕方させますね。findメソッドは型引数でUser
クラスを知っていそうに見えるけど、実際には型消去されてしまうので実行時型情報を取り出すことができず、仕方なく別口で引数として User.class をもらうようにしています。美しくない。
ジェネリッククラスやジェネリックメソッドの中でその型引数を実行時型情報として得る方法、ないものでしょうか。
わりと知られたやり方
「わりと知られた」なんて書くと煽りすぎすよねごめんなさい。それはさておき上記のfindメソッド、ちょっとした表技を使えば
List<User> result = Ebean.<User>find().findList();
という書き方でも実行時型情報を取れるようにできます。
それは、findの宣言を
static Query<E> find<E> (E... dummy) {
と書くこと。dummy は配列になりますからE[]
型です。配列は型消去されないので型情報が残っているという次第。
とは言え、この方法も限界があります。E
が総称型だと、E
の型引数は消えてしまいます。あと、Eclipseの内蔵コンパイラが誤動作するので困るなんて話も昔聞いたことがありますが今は大丈夫になったかなあ。なってるといいなあ。
型引数がすべて残る記法
今回ご紹介する、インスタンスを作成する場合にだけ使える記法です。
new ArrayList<Set<String>>(); // ==> 型情報は java.util.ArrayList<E>
new ArrayList<Set<String>>(){}; // ==> スーパークラスの型情報は java.util.ArrayList<java.util.Set<java.lang.String>>
なんか {}
を加えただけで型情報が急にリッチになりましたよ!?
お気付きでしょうか、後者は匿名クラスとしてのインスタンス作成ですが、これ、こう書いているようなものです。
class Temp extends ArrayList<Set<String>> { }
new Temp();
こう書かれてみると、Temp型が Set<String>
を知らなかったらむしろ不自然に思えてきます(だって、Temp
の中でvoid set(int i, Set<String> obj)
なんてメソッド書いたらそれはオーバーライドメソッドとして振る舞います。親クラスのことを型引数まで含めて知っていてこその振る舞い)。そして、実際知っているというわけ。
この手法は、複雑な総称型クラスを解析するライブラリで実際に使われています。総称型クラスを解析する必要のあるライブラリ、それはどんなのかというとひとつはJSONシリアライザですね。JacksonのTypeReference
クラスなんか、まさにこの手法を実現するためのヘルパです。