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ファイルにはしっかりとパラメタライズドタイプのパラメータは保持されているし、リフレクションでこのパラメータを読み出すことも出来る。

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()メソッドで判別できる

Java Generics Hell - 内部クラスと型変数のスコープ

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

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

Javaでは1ファイルにトップレベルのpublicなclassはひとつしか置けないが、入れ子になったクラスなどを用意することができる。種類がいくつかあるので後に整理するが、内部クラスの場合、外側のインスタンススコープの型変数が内部クラスの内側でも有効となる。

今回はそのあたりを整理してみよう。Javaのクラス宣言の種類については拙稿 Javaのクラス宣言5種+αで以前に取り上げているので参考にされたい。

static

内部クラスの話の前に、そもそも論としてクラスのインスタンススコープの型変数は、staticなメソッドや、フィールドでは用いることは出来ない。なぜなら、インスタンスをスコープとしているからだ。(トートロジー的な)

なお、コンストラクタはインスタンスメソッド扱いで型変数を使える。その他、static初期化ブロックはstatic扱いなので型変数を使えない。インスタンス初期化ブロックであればインスタンスの型変数を用いることができる。

トップレベルの同居したクラス

Javaではpublic/protectedなトップレベルのclassは .java ファイルにひとつしか置けないが、パッケージプライベートなclassは複数置くことができる。なお、privateなトップレベルクラスはそもそも作れない。例えばTopLevel.javaというファイルに以下の内容を記述するとコンパイルすることが出来、TopLevel.class と TopLevel2.class が作られる。

public class TopLevel<T> {
}
class TopLevel2 {
}

上記例のTopLevel2はトップレベルなので、当然ながら相互に型変数のスコープは独立している。つまり、考慮しなくて良い。

ネストしたクラス

ネストしたクラス (Nested class) は、class 内部に

  • static class を宣言したもの
  • interface を宣言したもの
  • enum を宣言したもの

である。

これらは、名前が「外側のクラス名. ネストしたクラス名」で扱われるが、基本的にトップレベルクラスと同等である。

public class TopLevel<T> {
	static class Nest1 {
	}
	interface Nest2 {
	}
	enum Nest3 {
	}
}

違いといえば、アクセスレベルをパッケージプライベートやprivateにすることができる。private宣言すると内部的にclassを作るものの、外部に露出させないことが可能だ。

これらもトップレベルクラスと同等で、外側のTopLevelの型変数Tをこれらネストしたクラス内で用いることは出来ない。

内部クラス

内部クラス(Inner class)は外部クラスのインスタンスに紐付く。そのため、外側のクラスのインスタンスの型変数を用いることができる。

public class TopLevel<T> {
	class Inner {
		T t;
		void hoge(T t) {}
	}
}

しかし、内部クラスといえども、内部クラスのstaticメソッドではTopLevelのTを扱うことは出来ない。

なお、内部クラスが3段以上になった場合、3段目では、1番外側のクラスで宣言された型変数、2段目で宣言された型変数の両方を用いることができる。実用上はそういうケースはなかなか発生しないとは思うが。

ローカル内部クラス

ローカル内部クラス(Local inner class)はメソッドやコンストラクタなどのブロック内で宣言されるクラス。このクラスはブロックがスコープとなるのだが、そのブロックで有効な外側の型変数はこのクラス内でも用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		class LocalInnerClass {
			S s; // メソッドスコープのSは使える
			// T t; インスタンススコープのTはNG
		}
	}
	/** インスタンスメソッド */
	<I> void instanceMethod() {
		class LocalInnerClass {
			I i; // メソッドスコープのIが使える
			T t; // インスタンススコープのTも使える
		}
	}
}

上記例では、staticメソッドではメソッドスコープのSは使えるが、インスタンススコープのTは使えない。対して、インスタンスメソッドでは両方を用いることができる。

無名クラス

無名クラス(匿名クラスとも呼ばれる)(Anonymous class)は主に抽象クラスやinterfaceなどに対して名前を付けずにその場で実装を書くもの。
これもローカル内部クラス同様にそのブロックで有効な外側の型変数はこのクラス内でも用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		Runnable run = new Runnable(){
			S s; // メソッドスコープのSは使える
			// T t; インスタンススコープのTはNG
			@Override
			public void run() {
				S s2;
			}
		};
	}
	/** インスタンスメソッド */
	<I> void instanceMethod() {
		Runnable run = new Runnable(){
			I i; // メソッドスコープのIが使える
			T t; // インスタンススコープのTも使える
			@Override
			public void run() {
				I i2;
				T t2;
			}
		};
	}
}

余談

ローカル内部クラスや無名クラスの中に内部クラスを作ると、外側のブロックで有効なメソッドスコープの型変数を内部クラスで用いることができる。

public class TopLevel<T> {
	/** staticメソッド */
	static <S> void staticMethod() {
		/** ローカル内部クラス */
		class LocalInnerClass {
			/** 内部クラス */
			class InnerClass {
				S s;
			}
		}
	}
}

内部クラスは、外側のクラスで利用可能な型変数は使える、ぐらいに理解しておくと良いだろう。え?こんな使い方することはないって?

Java Generics Hell - ジェネリックな例外

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

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

throws E

Javaジェネリクスの型変数は例外のthrows宣言でも用いることができる。型変数の宣言時にthrow可能な型であることを型変数の境界で示す必要がある。

public <E extends Exception> void hoge() throws E {}

上記はメソッドスコープの型変数 E を extends Exception としてみた。メソッドスコープの型変数だとthrowsに型変数を用いる意味があまりないが、型システムの挙動を見る分にはコード量が少なくて済むので都合が良い。

バインドの仕方でthrowされうる例外が変わる、例外が変わるのでcatchするべき例外も変わる。

// IOExceptionをバインド
try {
	this.<IOException>hoge();
} catch (IOException e) {
	// 略
}
// SQLExceptionをバインド
try {
	this.<SQLException>hoge();
} catch (SQLException e) {
	// 略
}
// RuntimeExceptionをバインド
// catch不要
this.<RuntimeException>hoge();

面白いのはRuntimeExceptionをバインドした場合は、catch不要となることだ。

AutoCloseableの例

ここで、java.lang.AutoCloseableを見てみよう。通常、メソッドのthrowsは具象型が用いられる。

public interface AutoCloseable {
    void close() throws Exception;
}

例えばjava.io.ByteArrayInputStreamでは

public class ByteArrayInputStream extends InputStream {
    // 略

    public void close() throws IOException {
    }
}

となっていて、ByteArrayInputStreamの具象型を扱っていてもclose()を呼ぶ際にIOExceptionが発生する扱いとなる。

byte[] buf = new byte[100];
try (ByteArrayInputStream bais = new ByteArrayInputStream(buf)) {
	// 略
} catch (IOException e) {
	// close() が throws IOException のため
}

throws Exceptionではなくthrows IOExceptionなのは、例外がメソッドからの出力であるため、より狭い、より具体的な型に絞っても良いからである。このあたりはジェネリクス以前からのJavaの型システムの機能性だが、ジェネリクスワイルドカードの話にも似ている。

というか、オーバーライドしてthrowsなくしておけばよかったじゃないか、という話もあるんだが、APIの歴史的な後の祭り。Java 1.0 のときに thrwos IOExceptionとしてしまったので、これをなくすとthrowされないIOEexceptionをcatchするコードがコンパイルエラーになってしまうので残されているのである。Javaは到達不能コードをコンパイルエラーにする方針なのでしょうがない。

話がそれた。

ここで言っておきたいのは、AutoCloseable型変数に一度代入してしまえば、そのclose()はthrows Exceptionだということだ。

byte[] buf = new byte[100];
ByteArrayInputStream bais = new ByteArrayInputStream(buf);
try (AutoCloseable ac = bais) {
	// 略
} catch (Exception e) {
	// IOException ではなく Exception になる
}

この点がジェネリクスなしでの不都合ということになる。

例外をインスタンス型変数にした場合

ではここにAutoCloseableとの対比としてinterface Foo を以下のように定めてみよう。

public interface Foo<E extends Exception> {
	void foo() throws E;
}

これをimplementsするclassは以下のようになる。

public class Bar implements Foo<IOException> {
	@Override
	public void foo() throws IOException {}
}

オーバーライドするにあたって throws IOException となった。
これでパラメタライズドタイプを用いて以下のように書ける。

Bar bar = new Bar();
Foo<IOException> foo = bar;

try {
    foo.foo();
} catch (IOException e) {
    // IOExceptionで済む
}

とはいえ、Fooがパラメタライズドタイプになって鬱陶しい。

なお、Foo<RuntimeException>であればcatch不要となる。

まとめ

例外のthrows宣言に型変数を用いることができるが、正直、あんまり活用できるポイントはない。

Java Generics Hell - 型変数のバインド

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

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

メソッドスコープの型変数

メソッドスコープの型変数へのバインドについては以前も書いたが再掲しておこう。

使用する場合は、通常はバインド型が推論されるのでそのまま呼び出せば済む。

Set<String> set = Collections.singleton("hoge");

型変数にバインドする型を明示する場合はメソッドの手前に山括弧で指定する。

Set<String> set = Collections.<String>singleton("hoge");

メソッドスコープの型変数については明示的にバインドしなくても概ね型が推論される点は知っておくと良い。これはジェネリクスが導入されたJava5当初からある機能だ。「型推論」というと変数宣言の際の型推論ばかりが挙げられがちだが(変数宣言時の型推論についてはJava10 で導入)変数宣言時のみならず、型が推論される部分はいくつかある点は注意されたい。

インスタンススコープの型変数

インスタンススコープの型変数の場合、バインドの仕方は主に2種類ある。ひとつはnewによるバインドで、もうひとつは継承によるバインドだ。

newによるバインドはおなじみであろう。

new ArrayList<String>();

といった場合の<String>の部分である。

インスタンススコープの型変数のバインドもJava7以降では型推論を用いることが出来る。

List<String> list = new ArrayList<>();

<>を書くことで型推論を用いますよ、と明示する。これがないとraw型扱いとなってしまう。この<>をダイヤモンドオペレーターと呼ぶ。

ダイヤモンドオペレーターは、Java5からあるメソッドスコープの型推論の機構をそのまま流用して作られている。おそらく型推論器があるから使い回すとちょっと便利じゃね?ぐらいの機能なのだろう。なお、Java8でStreamAPIを便利に扱うためにラムダ式が導入されたが、このラムダ式を便利に使うために型の推論器が強化され左辺側からの推論に強くなった。この影響で、Java7ではダイヤモンドでうまく推論できずエラーとなる場所でもJava8ならば用いることが出来ることがある。

継承によるバインド

もうひとつのバインド方法は継承の際のバインドである。

public class StringList extends ArrayList<String> {
}

継承する際に親クラス(上記例ではArrayList)の型変数に対してバインドすることが出来る。GoFのStrategyパターンのような継承を前提としたデザインパターンなどではこうした継承によるバインドを使うことがままある。

本稿では深入りしないが、再帰ジェネリクスの場合はnewではバインドできず、継承でのバインドしか行えない。

また、これも深入りしないが、継承でのバインドの場合、classファイルに型変数に何がバインドされたのか情報が残るためリフレクションによって型変数に何がバインドされたのか動的に確認することもできる。出来るからってそれを使って何かをやるというシーンは稀だと思うが。

バインド出来るもの

具象型でバインド出来るのは当たり前なのだが、パラメタライズドタイプでバインドすることも出来る。これは意識せずやっていることがあるだろう。

new ArrayList<List<String>>();

この例ではList<String>というパラメタライズドタイプをバインドしているわけだ。
また、スコープ的に有効であれば型変数でバインドすることも出来る。

public class Hoge<T> {
  public void piyo() {
    List<?> list = new ArrayList<T>();
  }
}

上記例ではインスタンススコープの型変数Tをインスタンスメソッド内でArrayListの型変数にバインドしている。

逆にバインド出来ないものとしては共変ワイルドカード、反変ワイルドカードのキャプチャをバインドすることは出来ない。

new ArrayList<? extends X>(); // NG
new ArrayList<? super X>(); // NG

これはパラメタライズドタイプでの山括弧には? extends, ? superが現れるため、見た目に紛らわしく思わず書けそうに思えてしまう。しかし、パラメタライズドタイプの山括弧とバインドの山括弧は別物であり、書けるものが異なるので意識して山括弧を見ることが重要だ。

Java Generics Hell - ワイルドカード落穂ひろい

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

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

共変ワイルドカードと反変ワイルドカードについて書いたので、残りの話題を拾っておこう。

Unbounded Wildcard

共変でも反変でもないただのワイルドカードとして<?>を見たことがあるのではないだろうか。
言語仕様上はUnbounded Wildcardと表現される。適当に訳すと無制限ワイルドカード、だろうか。

何が無制限かというと代入が無制限に行える。

List<?> list;
list = new ArrayList<String>();
list = new ArrayList<Integer>();

さて、前回、前々回の内容を把握している方であれば整合性を取るにはどうしたらよいか想像できるのではないだろうか。

  • Listのメソッドの引数のEに対してはnullしか渡せない
  • Listのメソッドの戻り値のEについてはObject型でしか受け取れない

おおよそ、? extendsと? superのツライところ取りみたいな形である。制限はきついが、代入は無制限なので、こうした制限でも構わなければ用いたほうがそのメソッドを利用しやすい。

Javaの標準APIでわかりやすい事例を挙げるとすればjava.util.Collections#disjointだろうか。

public static boolean disjoint(Collection<?> c1, Collection<?> c2)

指定された2つのコレクションに共通の要素が存在しない場合、trueを返す。それぞれのCollection ――これはjava.util.Listの親interfaceにあたる――から要素をObject型で参照できれば済む。

java.util.Collections#reverseは一見するとよい例のように見えるのだが

public static void reverse(List<?> list)

Listの順序を逆にするというものであるが、実は渡したlistを操作して順序を逆転させるのでadd()やset()でnullしか渡せないはずなのにどうしているかというとraw型を用いているということで例としてはあまり適切ではない。

raw型

Java5以降でJavaを学んだ人は基本的にジェネリクスの山括弧<>は省略してはいけない、警告は無視してはいけない、ということを徹底しておくとよい。

しかし、Javaは5以前、つまるところ1.0〜1.4までの間はジェネリクスがなかったわけで、構文上山括弧が用いられていなかった。Java5でそれらの時代のソースコードコンパイルできるように後方互換性として残されたものがraw型である。

List list = new ArrayList();

現代の標準的なコンパイラIDEの設定では警告が出ると思う。分かってて敢えて用いている場合はメソッドにアノテーション@SuppressWarnings("rawtypes")をつけると警告を除去できる。

raw型を用いると基本的に型の安全性が保証されない。明示的にダウンキャストして用いる必要が生じるが、扱いを誤れば実行時に java.lang.ClassCastException が出ることになる。そして、それをコンパイル時点では気づくことができない。

逆に言えば、Java1.4までの時代、java.util.Listなどのコレクションフレームワークを用いたソースコードは常にClassCastExceptionとの戦いであった。現代ではClassCastExceptionに遭遇することは稀であろう。

raw型は歴史的経緯という点が大きいが、変性の制御でどうにもならないときの逃げ道としては時に便利でもある。

Java Generics Hell - 反変ワイルドカード

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

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

反変ワイルドカード

さて、前回は共変ワイルドカードについてだった。今回は反変ワイルドカードについてである。

反変ワイルドカードは? superを用いて以下のように記述する。

List<A> listA = new ArrayList<A>();
List<? super B> listSuB = listA; // 代入可能

このlistSuBは次のような性質を持っている。

  • listSuB は B型を格納することが出来る
  • listSuB から取り出した型はObject型となる

前回のList<? extends A>型は、戻り値がA型であることが保証された。代わりに引数でA型オブジェクトを渡すことができなくなった。

List<? super B>型の場合は逆で、B側を引数として渡すことを保証する代わりに戻り値の型が保証できなくなるのだ。B型の親であるA型であることも保証できない。全ての型の始祖であるjava.lang.Object型としてだけget()することが可能となっている。

反変ワイルドカードの意義

メソッド内部では、共変ワイルドカードで渡されたオブジェクトから値を取ることが出来るのであった。対して、反変ワイルドカードで渡されたオブジェクトには値を渡すことが出来るのである。

考え方としてはサプライヤー、つまり、そのオブジェクトから何か値が供給される場合、? extends を用いる。対してコンシューマー、そのオブジェクトに値を渡して消費させる場合は? superを用いると良い。

事例

ワイルドカードの宝庫であるStreamAPIからjava.util.function.FunctionクラスのandThenを例に挙げよう。

以下ではclass A, B, C はAが親、Bが子、Cが孫である。またclass X, Y, Z ではXが親、Yが子、Zが孫である。

まずFunction<T,R>のインスタンス型変数はTが入力の型(汎用的にTypeの頭文字のTだろう)、Rが結果の型(これはResultの頭文字Rだろう)となっている。ここで、T型を受け取り、B型を返すFunction<T, B>を考えよう。

ここでandThenのメソッドシグニチャ

public <V> Function<T,V> andThen(Function<? super R,? extends V> after) { ... }

であり、V型はメソッドスコープの型変数である。ここでは簡単のためV型はY型としよう。
そしてFunction<T, B>であるので、

public Function<T,Y> andThen(Function<? super B,? extends Y> after) { ... }

と考えて良い。T型を受け取り、B型を返すFunction<T, B>に、B型を受け取りY型を返すFunction<B, Y>を与えると、くっつけてT型を受け取りY型を返すFunction<T, Y>にすることができる、というわけだ。

ここでandThenの実装コードを見てみよう。

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

RにBを、VにYをバインドして、実際の型のイメージで書き下ろすと以下のようになる。

default Function<T, Y> andThen(Function<? super B, ? extends Y> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

ラムダ式で書かれているが、T型の引数tを受け取ると、Function<T, B>のにtを渡してapply(t)の結果Bをさらにafterのapplyに渡す。今、Function<T, B>であるから、afterはB型を受け取って処理できる必要がある。

しかし、afterはB型だけではなく、親のA型を受け取ることが出来ても良い。しかし、孫のCしか受け取れないようではB型が渡されてくると困る。これが? super Bの効果。

そして、afterはY型を返さなくてはならない。しかし、実はZ型だけを返す実装でも良い。しかし、Yの親のXを返しては困る。これが? extends Yの効果。

このように、andThen(Function<B, Y> after)に対してandThen(Function<? super B, ? extends Y> after)としたほうが、afterの実装がより広く取ることができる。