「Java/ジェネリクス」の版間の差分
Semi-Brace (トーク | 投稿記録) ページの作成:「Javaにおけるジェネリクスは、Java 1.5から追加された。C++のテンプレートに「似た」概念で、ジェネリックプログラミングをサ…」 |
(相違点なし)
|
2021年4月13日 (火) 22:02時点における版
Javaにおけるジェネリクスは、Java 1.5から追加された。C++のテンプレートに「似た」概念で、ジェネリックプログラミングをサポートする。
概要
例えば、以下のクラスを考える:
class Box {
Object element;
Box(Object element) {
this.element = element;
}
}
そして以下のコードを考える。
class Main {
public static void main(String[] args) {
Box boxOfString = new Box("hoge");
Box boxOfInteger = new Box(Integer.valueOf(42));
unwrapBox(boxOfString);
unwrapBox(boxOfInteger); // !!! ClassCastException
}
/**
* Stringが格納されているBoxのelementを取り出し、標準出力に表示する。
* @param box Boxのインスタンス
*/
public static void unwrapBox(Box box) {
System.out.println((String) box.element);
}
}
このとき、6行目の呼び出しはunwrapBox
の呼び出し契約に違反している。なおかつ、Integer
はString
と継承関係がないため、無条件にClassCastException
という例外が送出される[注 1]。さらに、boxOfString
とboxOfInteger
が相互代入可能なことで、将来コード量が増えた時―あるいはコピーアンドペーストでコードを書いたときに取り違えるリスクがある。ここで、ジェネリクスを使用してBox
の定義、及びMain
のコードを一部修正する:
class Box<T> {
T element;
Box(T element) {
this.element = element;
}
}
Box<String> boxOfString = new Box("hoge");
Box<Integer> boxOfInteger = new Box(Integer.valueOf(42));
unwrapBox(boxOfString);
// unwrapBox(boxOfInteger); // コンパイルエラー
}
/**
* Stringが格納されているBoxのelementを取り出し、標準出力に表示する。
* @param box Boxのインスタンス
*/
public static void unwrapBox(Box<String> box) {
System.out.println(box.element);
}
}
山括弧の中に型が追加された。これを型変数と呼び、Box
については格納されている要素の型を表す。ジェネリクスを使用して、いくつかの利点を得た:
boxOfString
とboxOfInteger
を取り違えなくなった。unwrapBox(boxOfInteger)
でコンパイルエラーが発生するようになった。unwrapBox
でClassCastExceptionが送出される可能性がなくなった。
このように、ジェネリクスは型システムの範囲内にとどまりつつ、ある程度の柔軟さを追加する。ジェネリクスはList、Set、MapなどといったJava Collection Frameworkのメンバーを使用するときにほとんどと言っていいほど現れる。
raw型
ジェネリクス版Boxで、Box boxOfString = ...
と記述することもできる。これは1.4以前との後方互換性のために用意された機能で、raw型と呼ばれることがある。ジェネリックプログラミングの利点を損なう上、将来バージョンでは禁止になる可能性がある[1]とされているため、新規に書くコードでは使う理由がない。
共変性・反変性
型変数が追加されると厄介なことになる。例えば:
Box<String>
とBox<Integer>
の関係性は?Box<Number>
とBox<Integer>
の関係性は?
答えは「どちらも関係性がない」となる。Javaの型システムでは、それぞれ関係性がない別個の型とみなされる。これを非変という。しかし、これだけでは不便である。例えば、java.util.Listを使った以下のメソッドを考える[注 2]:
public static <E> void copyBox(Box<E> from, Box<E> to) {
to.element = from.element;
}
これはfrom
の中身をto
に代入。当然同じ型では動作する。しかし、copyList(dogBox, animalBox)
などとすると途端にうまくいかなくなる。これは合理的[注 3]なので、ぜひとも行いたいところだ。そこで、copyBox
を修正する:
public static <E> void copyBox(Box<? extends E> from, Box<? super E> to) {
to.element = from.element;
}
これでうまく行くようになった。? extends E
というのは、戻り値の部分にのみ型変数が出現し、代わりに共変になることを表す。? super E
というのは、引数の部分にのみ型変数が出現し、代わりに反変になることを表す。
つまり、Boxについて言えばこうだ:
- 共変
Derived
がBase
のサブタイプ→Box<Derived>
がBox<Base>
のサブタイプ- 反変
Derived
がBase
のサブタイプ→Box<Base>
がBox<Derived>
のサブタイプ- 非変
- 共変でも反変でもない→
Box<Base>
とBox<Derived>
に関係がない
PECS原則
PECSは、プロデューサー(producer)-extends, コンシューマー(consumer)-superを表しています。
―Effective Javaの項目28
修正後のcopyBox
を見ても反映されていることがわかる。
C++のテンプレート、C#とのジェネリクスとの違い
端的に言えば、実装方針の違いである。C++は、「テンプレートの実体化」と呼ばれるように、テンプレートに与えられた型の組み合わせだけ、(見えない) コードが生成される。他方、Javaのジェネリクスでは「イレイジャ」と呼ばれる方式でコードを一つにまとめている。具体的には、ジェネリクスを含んだメソッドやクラスは、実行時には型変数が全て展開され、シグネチャに型変数を含まない。ただし、classファイルにはメタデータとして型変数の情報が残っている。C#は共通言語基盤上でネイティブにジェネリクスをイレイジャなしで取り扱う[2]。
制約
型変数をnewに使えない
上述で説明したとおり、型変数は実行時には展開されてしまう。そのため、newする型が自動的に型変数の上限型となり、意味がない。コード中で出現した場合、コンパイルエラーを生成する。
型変数を配列の要素型に使えない
同上。コード中で出現した場合、コンパイルエラーを生成する。
型変数をinstanceof演算子の被演算子として使えない
同上。コード中で出現した場合、コンパイルエラーを生成する。
型変数に対してclassリテラルを呼ぶことが許されない
同上。コード中で出現した場合、コンパイルエラーを生成する。
Throwableの派生クラスは型変数を持つことができない
同上。
class GenericThrowable<T> extends RuntimeException {}
class Main {
public static void main(String[] args) {
try {
System.out.println("aaa");
throw new GenericThrowable<String>();
} catch (GenericThrowable<Integer> gti) {
throw gti;
} catch (GenericThrowable<String> gts) {
System.out.println("GenericThrowable<Strng>");
}
}
}
このコードはコンパイルできない。イレイジャにより型変数が「消える」のでコンパイラはどちらのcatch節へ行くべきか決定不能でもある。
注釈
出典
- ^ Java言語仕様第3版§4.8
- ^ https://ufcpp.net/study/csharp/sp2_generics.html#compare 20210414