Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2017-12-11

Java Generics Hell - 型変数の境界

| 20:54 |  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の多重実装

Javaclassの単一継承interfaceの多重実装、という継承ポリシーだが、ジェネリクス継承階層を考える際はclassinterfaceかは区別する必要はほぼない。継承に際して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の言語仕様的には型変数の制約は継承関係しか表現できないが、他の言語ではより多彩な制約を課すことが出来るものもある。興味があれば型クラスなどについても調べてみると良いだろう。