Java Generics Hell - ブリッジメソッド

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

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

共変戻り値

Java5以降ではメソッドをオーバーライドするときに、戻り値をより具体的な型としてオーバーライドすることが許されている。

public interface Parent {
    Number getValue();
}

このjava.lang.Number型を返すgetValue()メソッドをChild型でオーバーライドするときにNumberの子であるInteger型にすることができる。

public class Child implements Parent {
    public Integer getValue() {
        return 0;
    }
}

これが共変戻り値だ。

ジェネリクスを用いている場合に、継承でバインドすると同様の共変戻り値となることがある。

public interface Parent<T> {
    T getValue();
}
public class Child implements Parent<Integer> {
    public Integer getValue() {
        return 0;
    }
}

bridge メソッド

これらのケースで、Child型のclassファイルを覗くと面白いものが見える。

>javap Child.class
Compiled from "Child.java"
public class Child implements Parent {
public Child();
public java.lang.Integer getValue();
public java.lang.Object getValue();
}

getValue()がふたつあるのが分かるだろうか。Eclipseのclassファイルビューアだと以下のように見える。

// Method descriptor #24 ()Ljava/lang/Object;
// Stack: 1, Locals: 1
public bridge synthetic java.lang.Object getValue();
0 aload_0 [this]
1 invokevirtual Child.getValue() : java.lang.Integer [25]
4 areturn
Line numbers:
[pc: 0, line: 1]

bridge syntheticと記載があるのが分かるだろうか。これが今日のテーマ、ブリッジメソッド(bridge method)だ。

引数のブリッジ

さて、共変戻り値の例を挙げたが、型変数を引数にとる場合でも同様の事例が発生する。

public interface Parent<T> {
	void xxx(T t);
}
public class Child implements Parent<String> {
	@Override
	public void xxx(String t) {
	}
}

ここで型変数Tは境界がないので、Parentのメソッドxxxの実際のシグニチャはxxx(Object)となっている。

Childでは型変数を継承でバインドしてStringとしているので、Parentのメソッドxxx(Object)をオーバーライドしてxxx(String)としてしまっている。

Javaでは呼び出すメソッドを決めるときに「メソッド名」と「引数の型」が必要だった(より正確には引数の型のイレイジャとなる。拙稿贖罪のイレイジャを参照されたし)。「メソッド名」が同一で「引数の型」が異なる場合、オーバーロードとなることはJavaの初歩で習うことだ。しかし、ここではxxx(Object)とxxx(String)となっている。

そこで、こうしたケースで、もとのメソッドシグネチャであるxxx(Object)を「ブリッジメソッド」として、xxx(String)を呼び出す実装をコンパイラが作り出す。

リフレクション

こうしたブリッジメソッドはリフレクションでメソッド一覧を取得したようなときにも現れる。

そこでjava.lang.reflect.Methodクラスにはブリッジメソッドであることを判別するためのisBridge()が用意されている。

また、似たような話題としてisSynthetic()というメソッドも用意されている。これは合成(synthetic)されたメソッドにつくフラグで、このブリッジメソッドの他にもコンパイラによって生成されたメソッドに立てられる。

Java言語仕様第3版 13.1 Javaバイナリの形式 (299ページ)から抜粋すると

コンパイラによって導入された,ソースコード中に対応する構造のない任意の構造は,デフオルトのコン
ストラクタとクラス初期化メソッドを除いてすべて,合成(synthetic)されたものである旨記録されなけ
ればならない。

と書かれている。先のclassファイルビューアにあったbridge syntheticの記載の意味はコンパイラによって作られたブリッジメソッドというフラグだったというわけだ。

ブリッジメソッドの衝突

ところでブリッジメソッドが衝突するとどうなるのか?

public interface Parent<T> {
	void xxx(T t);
	void xxx(String s);
}
class Child implements Parent<String> {
	@Override
	public void xxx(String t) {
	}
}

この場合はこのままコンパイルに成功する。ちょっと面白い現象だ。

まとめ

  • 共変な戻り値や、型変数を引数にして継承でバインドした場合などで、オーバーライドにも関わらずメソッドシグネチャが異なるケースが生じる
  • こうしたケースでは親クラスのシグネチャと同等な「ブリッジメソッド」がコンパイラによって作られ、オーバーライドした具象型のメソッドへブリッジする
  • ブリッジメソッドであるかどうかはjava.lang.reflect.MethodクラスのisBridge()メソッドで判別できる