Java Generics Hell - イレイジャ

Java Generics Hell アドベントカレンダー 22日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

メソッドのオーバーロード

Javaジェネリクスのイレイジャについて語るには、まずメソッドのオーバーロードについて語らねばなるまい。

メソッドのオーバーロードとは、同名で引数の型違いのメソッドのことである。

public class Hoge {
	public void foo() {}
	public void foo(String s) {}
}

Javaではなぜ同名のメソッドを宣言することが出来るのであろうか?コンパイラが、あるいはJavaのランタイムであるJavaVMが、メソッドを特定するときに

  • 属するクラスの完全修飾名
  • メソッド名
  • 引数の型の並び

によってどのメソッドを呼ぶか特定しているのである。

これはリフレクションAPIにも現れていて、Methodを取得しようとした場合には

Class<Hoge> clazz = Hoge.class;
Method method = clazz.getMethod("foo", String.class);
System.out.println(method);

といったように

  • 対象となるClassオブジェクトを取得してClass#getMethod()を呼ぶ
  • 引数にはメソッド名と
  • 引数の型の並びをClassの配列で渡す(可変長配列)

とする必要がある。

プログラム言語によってはオーバーロードを許していないものもある。オーバーロードを許さない言語では、メソッドを特定するのは楽で、

  • 属するクラス
  • メソッド名

だけが分かればよい。メソッドを呼び出すためにいちいち引数のクラスの羅列を用意する必要はなくなる。

イレイジャ

さて、オーバーロードについて振り返った。ジェネリクスのイレイジャを理解するにはオーバーロードについて振り返っておく必要があるのだ。Javaではメソッドを特定するために

  • 属するクラスの完全修飾名
  • メソッド名
  • 引数の型の並び

が必要だと言ったが、この「引数の型の並び」はイレイジャである必要がある。

ここで、イレイジャの定義についてJava言語仕様より抜粋してみよう。

4.6 型のイレイジャ
型のイレイジャ(type erasuer:型消去)とは,型(パラメータ化型や型変数を含む)から型(パラメータ化型や型変数を含まない)への対応付けである。型Tのイレイジャは|T|と表記される。イレイジャの対応付けは以下のように定義される。
・パラメータ化型(§4.5) Gのイレイジャは|G|である。
・ネストされた型T.Cのイレイジャは|T|.Cである。
・配列型Tのイレイジャは|T|である。
・型変数(§4.4)のイレイジャは,その最も左端の境界におけるイレイジャである。
・その他の型すべてのイレイジャは,その型自身である。
メソッド・シグネチャsのイレイジャは,sと同じ名前,およびs中で指定されたすべての形式的パラメータ型のイレイジャからなるシグネチャとなる。

「パラメータ化型」という単語が出てくるが本稿シリーズでは「パラメタライズドタイプ」と呼んでいる。つまり、List<String>のような型のことである。なので、先程のメソッドを特定するための条件は正確には

  • 属するクラスの完全修飾名
  • メソッド名
  • 引数の型のイレイジャの並び

ということになる。

このことによる直接的なデメリットは、引数のイレイジャが同じになるメソッドのオーバーロードが許されないということである。

これがもし、メソッドを特定するために

  • 属するクラスの完全修飾名
  • メソッド名
  • 引数の型(パラメタライズドタイプのパラメータも含む)の並び

となっていれば、引数の型のさらにパラメータの違いによってメソッドの区別が出来ることになる。もちろん、そのためにコンパイラやランタイムはいちいちメソッドを特定するためにそれだけの情報を参照する必要が生じるということでもある。

歴史的な経緯を言えば、Javaは1.4から5の間でジェネリクスが追加されたのであった。この時、メソッドを特定する仕組みを旧来のものと互換をとった。それがイレイジャ方式というわけである。

イレイジャ方式のメリットは、互換性のキープもあるが、リフレクションなどで扱う際に煩雑になりすぎない点がある。これは実行時の型エラーと表裏一体のものではあるが、JavaVM上でより拡張された型システムを構築する場合(つまりそのような拡張された言語を作るような場合)にやりやすいということでもある。

よくある誤解

Javaはメソッドの特定の際にイレイジャを用いて特定するということは前節で述べたが、Javaのclassファイルに「イレイジャしか格納されていない」わけではない

これは、引数にList<String>を取るメソッドがあったとして

public class Hoge {
  public void foo(List<String> list) {}
}

これをコンパイルしてHoge.classファイルを作る。

次に、このHoge.classファイルだけをもってきてクラスパスを通し、別のクラスからこのメソッドfooを呼び出す。

public class Main {
  public static void main(String[] args) {
    Hoge hoge = new Hoge();
    List<String> list = new ArrayList<String>();
    hoge.foo(list);  // OK

    List<Integer> list2 = new ArrayList<Integer>();
    hoge.foo(list2);  // NG
  }
}

この時、hoge.foo(list);はコンパイルが通るとして、パラメタライズドタイプのパラメータが異なるhoge.foo(list2);はコンパイルエラーとなる。classファイルにfooの引数がList<String>のイレイジャ|List|だけが残されているのだとしたら、引数に誤ってList<Integer>を渡そうとした場合にコンパイルエラーに出来ないはずである。

しかし、現実にはコンパイルエラーになる。classファイルにはしっかりとパラメタライズドタイプのパラメータは保持されているし、リフレクションでこのパラメータを読み出すことも出来る。