Javaのジェネリクスでしばしば話題に上がる「イレイジャ」について整理しておきたい。

イレイジャについては僕もいろいろと誤解しており、過去に誤った発言をしている。本エントリはその贖罪として書かれたものである。

「イレイジャ」という方式についてはネガティブな誤解が広まっていると思う。「イレイジャ方式」が問題の根ではない事象について、それを「イレイジャのせい」であると誤って理解することはエンジニアとしてはマイナスである。

しばしばイレイジャのせいとされる事象にnew T()できないという論点があるが、これはJavaのジェネリクスがC#でいうnew制約(型変数の制約としてデフォルトコンストラクタを持つことを要求する機能)を持たないことに起因する問題である。

そのため、この点についてJavaの言語仕様に改善を求めるのであれば、new制約を導入せよという現実的な要求とするべきである。

イレイジャ方式を採用したのは歴史上、Javaが初めてというわけでもなく、Java VM と .NET の IL がよく対比されるのは Java / .NET がそれだけ多く利用されているからなのだろう。

イレイジャの理解 イレイジャを理解するために、類似性のあるものとしてメソッドのオーバーライド / オーバーロードを例に挙げよう。 Javaのメソッドは「メソッド名」と「引数の型」によって特定される。 プログラミング言語の種類によってはメソッドを「メソッド名」のみで特定して用いる言語もあり、その場合は同名で引数の型違いのメソッドを扱うことができない。 Javaの型(class / interface)は「型名」によって特定される(より正確にはパッケージ名を含む完全名のことである)。同名で型変数違いの型を扱うことはできない。 その「型名」をより厳密に定義したものが先に挙げた「イレイジャ」であり、 型Tのイレイジャは|T|と表記される に従えば、例えばjava.util.ArrayList<String>のイレイジャは|java.util.ArrayList|であり、java.util.ArrayList<Integer>のイレイジャもまた|java.util.ArrayList|である。 Java5以前であれば「型」を特定するのに型の完全名さえあれば十分であった。Java5によってジェネリクスが導入されるとそれまでの「型」に「型変数」という要素が増えた。 この時に「型」＋「型変数」で特定するような変更を行わなかった。「型」＋「型変数」に対応づく「型（パラメータ化型や型変数を含まない）」つまり「イレイジャ」を使って特定することとし、既存の機能との互換性を保つ方針としたわけである。|java.util.ArrayList|で解決するわけだ。 これはメソッドのシグネチャについても波及して、hoge(List<String> list)とhoge(List<Integer> list)というようなオーバーロード定義ができないことに繋がる。いずれのイレイジャも|java.util.List|であるから。

実行時のイレイジャ Javaは実行時に用いるjava.lang.Classの情報をパラメタライズド・タイプ毎に持たない。ClassLoaderがClassを読み込む際には代表してイレイジャが用いられる。 そのため、パラメタライズド・タイプを多用してもClassが大量に読み込まれてMetaspace(Java7までだとPermanent領域)が圧迫される心配はない。 しかし同時に、実行時にObject#getClass()で得られるjava.lang.Classもイレイジャなのである。VM内でClassのロードやメソッドシグネチャの解決をするにはイレイジャがあれば十分なので、getClass()の機能性もJava1.4まで同等としたのだろう。 この点が語感もあって「イレイジャ」の本質であると誤解している人が多いのではないか。 実行時に消すから「イレイジャ」なのではなく、シグネチャ解決のためにイレイジャを用いるから動的に持ちまわっている情報もイレイジャまでで済ませている、ということである。 まぁ腐して言う人にしてみれば理由はなんであれ実行時に型が取れなければ一緒だろう :-P

イレイジャ方式のScala Java VM 上で動くJavaより後の世代の言語に Scala が挙げられる。Scalaもまたイレイジャ方式のメソッド解決をする言語である。 Javaと同様にイレイジャが同一となるメソッドのオーバーロードができないという制約を持つ。 しかし、TypeTag / ClassTagを用いて実行時に型変数の型を得ることができる。つまりイレイジャ方式であっても、実行時に型変数の型を動的に得られるようにすることは可能である。 Javaが実行時に型変数の型を動的に得られないのは、実行時のイレイジャに対して、別途、型変数の型を渡す機構を作らなかったからと言えよう。現に、別途作ったScalaはやれているのだから。

実行時の型変数 実行時に動的に型変数の値を得られるようにするにはいくつかの方法論が考えられる。ジェネリクスの実装方式も含めていくつか挙げると シグネチャの解決に型変数も利用するようにする (非イレイジャ)

コンパイル時に型のぶんだけコードの複製を作る (テンプレート方式)

メソッド呼び出しに際してコンパイラが暗黙の変数を渡す (Scala方式) これらはどれが一概に優れているとは言えない。いずれの方式でも相応の実用性を得ることができるだろうし、それぞれデメリットもある。 Javaはジェネリクスを導入するにあたって互換性を考慮してイレイジャ方式を採用したため、実行時に動的に型変数の値を得られるようにするには、必然的にScalaのような方式を取らざるをえないのであるが。 ただ、そもそも、ジェネリクスを静的な型チェックに重きをおくならば、実行時に動的に得る機能性というコードを書くことは下策なのである。だが、時にこの抜け道が役に立つ時がある。しかしこれはあくまでも抜け道であって、抜け道があることをジェネリクスの本質なのだ、などという主張は説得力に欠ける。

イレイジャ方式のメリット イレイジャ方式のメリットとして挙げられるものは ジェネリクス対応前のコードとの互換性

同VM上でより拡張された型システムの実装を設計する際の容易さ があるだろう。 既存のVMに乗っかる形での言語拡張は、どうしてもその土台の言語のランタイムに引きずられる部分がある。型システムをより先進的なものに拡張しようとした場合、土台の言語のランタイムの型システム上にのせる必要がある。 この時、ランタイムが型＋型変数(ただし土台の言語の型システムの型)にて解決する仕様だとすれば、より先進的な型を土台の言語のより古い型＋型変数に対応付けなくてはならない。これは正により先進的な型システムから型情報を消し去る対応付けなわけで、イレイジャである。そして、完全なイレイジャではなく、土台の言語の型システムの型へと対応付ける部分欠損するイレイジャということになる。この部分欠損するイレイジャは、完全欠損するイレイジャに比べ扱いが面倒くさいものになることが想像できることだろう。 つまり、土台を変えない前提ならイレイジャとせざるを得ないのである。そして不完全なイレイジャが面倒ごとを持ってくることは容易に想像できるだろう。そのときにランタイムを一新する痛みを我慢できるかどうか、である。