以下のような例を考える。
class Bot { public Object echo(Object obj) { return obj; } }
しかし、以下のように戻り値がString型と分かっているような場合、毎回キャストするのは非常に手間だしコストにもなる。
Bot bot = new Bot(); String str = (String) bot.echo("Hello");// キャストする必要がある
型安全でなく意図した型と違う型が帰ってくる可能性もある。そこで以下のようにジェネリック型を用いるとスマートになる。
class Bot<T> { public <T> echo(T obj) { return obj; } }
Bot<String> bot = new Bot<String>(); String str = bot.echo("Hello");
■例
以下のような場合、Eは型変数と呼ばれる。
class Cell<E> { private Cell<E> next; private E element; public Cell(E element) { this.element = element; } public Cell(E element, Cell<E> next) { this.element = element; this.next = next; } public E getElement() { return element; } public void setElement(E element) { this.element = element; } public Cell<E> getNext() { return next; } public void setNext(Cell<E> next) { this.next = next; } }
型変換について
慣例として以下のような法則で用いられる。
- 要素
- E – element
- キー
- K – key
- 値
- V – value
- 一般的な型
- T – type
以下のように型の情報は2箇所必要になる。
Cell<String> cell = new Cell<String>("Hello");
但し、Java 7では以下のように省略することができる。
Cell<String> cell = new Cell<>("Hello");
Java 7について
switch
以下のように文字列が条件判定に使えるようになった。
switch(str) { case "hoge": hoge(); case "fuga": fuga(); case "piyo": piyo(); }
マルチキャッチ
以下のように同時に複数の例外をキャッチできる。
try { // code } catch(IOException | SQLException e) { }
class SingleLinkQueue<E> { protected Cell<E> head; protected Cell<E> tail; public void add(E item) { Cell<E> cell = new Cell<E>(item); if(tail == null) { head = tail = cell; } else { tail.setNext(cell); tail = cell; } } public E remove() { if(head == null) { return null; } Cell<E> cell = head; head = head.getNext(); if(head == null) { tail = null; return cell.getElement(); } } }
以下のようなクライアントコードとなる。
SingleLinkQueue<String> queue = new SingleLinkQueue<String>(); queue.add("Hello"); queue.add("World");
以下のようにするとコンパイルエラーとなる。
SingleLinkQueue<String> queue = new SingleLinkQueue<String>(); queue.add(1);
■ジェネリック型宣言
class Cell<E> {}
以下のように複数の型パラメータを含むことができる。
interface Map<K, V> {}
以下の2つはコンパイラに対して別のクラスを生成するように指示しているのではなくあくまでも同じクラスであり2つのジェネリック型の呼び出しで、2つの異なるオブジェクトに対して2つの異なる方法で適用されるに過ぎない。
Cell<String> strCell; Cell<Number> numCell;
以下のようにクラスは”Cell”だけであり<String>や<Integer>はクラス定義ではなくオブジェクトの型についての情報を宣言しているだけである。
Cell<String> strCell = new Cell<String>("Hello"); Cell<Integer> intCell = new Cell<Integer>(25); boolean same = (strCell.getClass() == intCell.getClass());
制限
型パラメータをEとすると以下の場所ではEを使用することはできない。
- staticフィールド
- staticメソッド内
- static初期化子内
Cell<String>やCell<Integer>は1つのクラス=>任意のstaticフィールドを共有する=>パラメータによって別の型を指定される=>任意のstaticフィールドは共有できず別のフィールドとなる=>矛盾
配列の生成にはコンパイラがどのようなコードを生成するか知っている必要があるので、型パラメータEをインスタンス化したり、Eの配列を生成したりはできない。
■境界のある型パラメータ
interface SortedCollection<E extends Comparable<E>> { }
■ネストしたしたジェネリック型
class SingleLinkQueue<E> { static class Cell<E> { private Cell<E> next; private E element; /* ... */ } protected Cell<E> head; protected Cell<E> tail; /* ... */ }
以下のように内部クラスである場合、直接型変数にアクセス可能である。
class SingleLinkQueue<E> { class Cell { private Cell<E> next; private E element; /* ... */ } protected Cell<E> head; protected Cell<E> tail; /* ... */ }
この場合、内部クラスをジェネリック型にすると外部の型変換は隠蔽されるので好ましくない。
■サブタイプとワイルドカード
static double sum(List<Number> list) { double sum = 0.0; for(Number n : list) { sum += n.doubleValue(); } return sum; }
IntegerはNumberのサブタイプであるがList<Integer>はList<Number>のサブタイプではないので以下のように記述することはできない。
double sum = sum(new ArrayList<Integer>());
以下のようにすることで、この問題を解決できる。
static double sum(List<? extends Number> list) { double sum = 0.0; for(Number n : list) { sum += n.doubleValue(); } return sum; }
?は型引数の境界ワイルドカードでありNumberは上限境界である。
下限境界ワイルドカード
以下のようにすると?はIntegerもしくはそのsuperクラスである。
List<? super Integer>
非境界ワイルドカード
List<?>
List<? extends Object>
ワイルドカードの制限
以下のように型が分かっていないとできないことはできない。
SingleLinkQueue<?> strings = new SingleLinkQueue<String>(); strings.add("Hello");
一方で下限境界ワイルドカードでは問題ない。
static void addString(SingleLinkQueue<? super String> sq) { sq.add("Hello"); }
■ジェネリックメソッドとジェネリックコンストラクタ
以下のように記述した場合、SingleLinkQueue<String>のインスタンスに対して、E[]はStringの配列となるが、Objectの配列に格納することはできない。
public E[] toArray(E[] ary) { int i = 0; for(Cell<E> c = head; c != null && i < ary.length; c = c.getNext()) { ary[i++] = c.getElement(); } return ary; }
以下のようにすることで解決できる。
public <T> T[] toArray(T[] ary) { Object[] tmp = ary; int i = 0; for(Cell<E> c = head; c != null && i < ary.length; c = c.getNext()) { tmp[i++] = c.getElement(); } return ary; }
■ジェネリック呼び出しと型推論
以下のようにメソッドを定義する。
<T> T passThrough(T obj) { return obj; }
以下のようにして呼び出すことができる。
String str1 = "Hello"; String str2 = this.<String>passThrough(str1);
ちなみに以下のようにメソッド名だけを使用する呼び出しはできない。
String str1 = "Hello"; String str2 = <String>passThrough(str1);
メソッド名は適切に修飾する必要がある。
しかし、以下のようにして型の記述を省略することができる。
String str1 = "Hello"; String str2 = this.passThrough(str1);
これが型推論である。
String str1 = "Hello"; Object o1 = passThrough(s1);// T => String Object o2 = passThrough((Object) s1);// T => Object
■ワイルドカードキャプチャ
以下のように記述するとコンパイルすることはできない。
SingleLinkQueue<?> strings = new SingleLinkQueue<String>(); strings.add("Hello");
SingleLinkQueue>の推定される型は<capture of ?>である。従って、addへのパラメータも<capture of ?>であるがStringと互換性はないのでエラーとなる。
■イレイジャと原型
コンパイラがコンパイルされたクラスから全てのジェネリック型の情報を消す処理をイレイジャという。ジェネリック型あるいはパラメータ化された型のイレイジャは飾りのない型名で原型と呼ばれる。型変数のイレイジャは最初の上限境界となる。
オーバーロードとオーバーライド
以下のように記述すると同じシグネチャをもつメソッドとみなされるために、オーバーロードすることはできない。
public class Sample<E> { void m(E t){} void m(Object t){} }
以下のようにしてオーバーライドすることができる。
public class Sample<E> { void m(E t){} class Hello extends Sample<Object> { @Override void m(Object t){} } }
一見すると同じシグネチャかどうか見分けにくいので注意が必要である。
■クラス拡張とジェネリック型
以下のようにすることはできる。
class RemoteEventService extends AbstractEventService {}
class LocalEventService<T extends Event> extends AbstractEventService {}
しかし、以下のように記述するとすることはできない。Comparable<Value>とComparable<ExtendedValue>を実装することになりコンパイルすることはできない。
class Value implements Comparable<Value> {}
class ExtendedValue extends Value implements Comparable<ExtendedValue> {}