Java Generics Hell - 型変数の境界

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

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

週末サボりました。すいません。

型変数の境界

ここまでは話を簡単にするために型変数の境界については触れずにきた。

しかし、型変数の境界についてはそこまで難しい話ではない。その型変数が何かを継承していることを制約としてつけることができる、というだけである。

public class Hoge<T extends Piyo> { }

ここで、TはPiyo型かもしくは、Piyo型を継承した何かでなくてはならない。この時、class Hogeの中で型変数T型の変数はPiyoを継承していることが保証されるので、Piyoに定義されるメソッドを呼び出すことができる。これを型変数の境界という。なお、指定できるのはextendsだけである。

public class Piyo {
  public void xxx();
}

public class Hoge<T extends Piyo> { 
  T piyo;

  public void foo() {
    piyo.xxx();  // ← Piyo#xxx()が使える
  }
}

この場合、Hoge型を外から見た場合は(パラメタライズドタイプの場合のこと)、TはバインドされたPiyoの具象型として見える。
対して、Hoge型の内部としては、Tはあくまで「Piyoを継承した何か」だと思って扱わなければならない。

Tが具体的に何の型であるかinstanceofして分岐したい、と考えたのなら、それは設計がマズいと考えるべきである。端的にはうまく抽象化が出来ていないということだ。

interfaceの多重実装

Javaはclassの単一継承、interfaceの多重実装、という継承ポリシーだが、ジェネリクスで継承階層を考える際はclassかinterfaceかは区別する必要はほぼない。継承に際してclassの宣言時、interfaceの宣言時の書き方が異なるからか、この点で余計な混乱をしている人を見かけたので敢えて書くが、「継承階層」を考える際は一緒くたでいい。一緒くたで考えるとJavaの継承階層というのは、多重継承ということになる。

型変数の境界には、複数のinterfaceを継承していることを制約とすることもできる。

public class Hoge<T extends Cloneable & AutoCloseable> { }

記法は以上の通りで & で複数のinterfaceを並べることができる。

再帰的な宣言

型変数の境界にパラメタライズドタイプを用いることも出来る。

public class Hoge<T extends Comparable<String>> { }

パラメタライズドタイプのバインドに宣言中の型変数を使うことも出来る。

public class Hoge<T extends Comparable<T>> { }

ついでに挙げれば宣言中のcalss 自体も使うことが出来る。

public class Enum<E extends Enum<E>> { }

これはjava.lang.Enumなどに使われる再帰ジェネリクスというテクニックだが、効能については別稿としよう。

java.lang.Voidの憂鬱

型変数のバインドに際して、「使用しない」というケースが稀にある。こうした時、一般にはjava.lang.Void型で型変数を潰す。

Map<String, Void> map = new HashMap<String, Void>();

java.lang.Voidはvoidのラッパー型なので、型変数にvoidを指定したいときにも用いられる。しかし、JavaのVoidはあくまでもただのclassでしかなく、特別な型というわけではない。そのため、型変数に境界が指定されている場合、Voidをバインドすることができない。

public class Hoge<T extends Piyo> { }

Hoge<Void> hoge = new Hoge<Void>(); // ← NG

これはVoidがextends Piyoではないことによる。こうした型変数に境界があるケースで、かつ、Voidのようなもので潰すことが想定されるような場合は、自力でVoid用のクラスを定義してやる必要がある。Scalaなどでは「全ての子」に相当する型が規定されているのでこうした苦労がない。

ちなみに、Javaのnullは構文的には全ての型にキャスト可能なnull型の唯一のリテラルという扱いで、イメージ的にはnull型は全ての型の子ということになる。しかしこのnull型(null type)というのは言語仕様上現れはするものの、Javaのコードでこのnull型を明示的に扱うことはできない(リフレクションでも扱えない)ので型変数をnull型で潰すような使い方はできない。通常はnull型というのは概念上のもの、ぐらいに思っていて良いだろう。(JVMの中とかでは現れるかもしれない)

まとめ

  • 型変数に継承関係の制約をつけれる
  • 複数のinterface を実装していることを制約とすることも出来る
  • java.lang.Voidで型変数を潰すようなときに型変数に境界があるとJavaの言語仕様では困る

なお、Javaの言語仕様的には型変数の制約は継承関係しか表現できないが、他の言語ではより多彩な制約を課すことが出来るものもある。興味があれば型クラスなどについても調べてみると良いだろう。