ジェネリクス: 基礎と価値
✅
List<T>
になっていると T
をあとで決められる
List
を実装して提供する側の立場になると、何を複数持ちたいか知らないまま実装できる
ここに融通が効かないと StringList
や NumberList
をすべての型に対して用意しておかなければいけなくなるし、User
のようなプロジェクト固有の型に対しては先に用意できない
✅
List
という「複数保持できる」という特性と T
という「何を」を分けられる
組み合わせることで新たなクラス定義をすることなく様々なことを表現できる
✅
List
Option
Future
Either
自分で作ってもいい
🙅♂️
ジェネリクスはコンパイル時に確認されるが、コンパイル結果からは消される ( Type Erasure? )
f(List<String> list)
と f(List<Integer> list)
をオーバーロードできないのはそのため
🙅♂️
f
に注目
public static void main(String[] args) {
List<String> strings1 = Arrays.asList("Alice", "Bob");
List<String> strings2 = Arrays.asList("Christina", "David", "Eddy");
f(strings1, strings2);
}
private static <T> void f(List<T> list1, List<T> list2) {
T t1 = list1.get(0);
T t2 = list2.get(0);
System.out.println(list1.size()); // 2
System.out.println(list2.size()); // 3
}
1
f
目線だと何のリストかはわからない、なので get
で取り出しても T
であることしかわからない
ここでわかることはせいぜい list1
から取れるものと list2
から取れるものが同じ型であることだけ
size
のような中身を問わない List
そのもののメソッドは当然呼べる
2
1 でも触れたが T
は「何かは知らないが T
と書いてあるところは全部同じ型」ということ
なので List<String>
と List<Number>
を渡したりはできない
違ってもいいようにしたいならこうする
public static void main(String[] args) {
List<String> strings1 = Arrays.asList("Alice", "Bob");
List<String> strings2 = Arrays.asList("Christina", "David", "Eddy");
List<Number> numbers = Arrays.asList(1, 3, 5);
f(strings1, strings2);
f(strings1, numbers);
}
private static <T1, T2> void f(List<T1> list1, List<T2> list2) {
T1 t1 = list1.get(0);
T2 t2 = list2.get(0);
System.out.println(list1.size()); // 2
System.out.println(list2.size()); // 3
}
T1
と T2
は違ってもいいし、同じでもいい
ただし f
目線だとどう渡されるかはわからないため、f(strings1, strings2)
と呼んだからと言って f
の中で T1
と T2
を同じ型として扱えるわけではない
T
ではなく ?
でもいいが
T
ではあまりに情報がなくて役に立たない、せめて「数字」であることは保証したい
みたいなケースのために、変性について理解する
✅
T
は同じなので List<String>
を渡せば String
が返ってくると確定する
List<Object>
と Object
を受け取るようにしてもコンパイルはできるが、Object
としてしか返せないのであまり役に立たない
✅
T
でも E
でも R
でもいい
✅
型引数とかの説明
✅
map
などの紹介
🙅♂️
Element
のような単語でもいい
本来は抽象化するためのものであまり具体的なことはわからないので、意味のない一文字を使うのが基本
T1, T2, T3, T4
みたいになってしまうと複雑なので名前をつけることもある
Rust を参照