Open12

ジェネリクス: 基礎と価値

ほげさんほげさん


List<T> になっていると T をあとで決められる

List を実装して提供する側の立場になると、何を複数持ちたいか知らないまま実装できる

ここに融通が効かないと StringListNumberList をすべての型に対して用意しておかなければいけなくなるし、User のようなプロジェクト固有の型に対しては先に用意できない

ほげさんほげさん


List という「複数保持できる」という特性と T という「何を」を分けられる

組み合わせることで新たなクラス定義をすることなく様々なことを表現できる

ほげさんほげさん

🙅‍♂️
ジェネリクスはコンパイル時に確認されるが、コンパイル結果からは消される ( 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
}

T1T2 は違ってもいいし、同じでもいい

ただし f 目線だとどう渡されるかはわからないため、f(strings1, strings2) と呼んだからと言って f の中で T1T2 を同じ型として扱えるわけではない

ほげさんほげさん

T ではあまりに情報がなくて役に立たない、せめて「数字」であることは保証したい

みたいなケースのために、変性について理解する

ほげさんほげさん


T は同じなので List<String> を渡せば String が返ってくると確定する

List<Object>Object を受け取るようにしてもコンパイルはできるが、Object としてしか返せないのであまり役に立たない

ほげさんほげさん

🙅‍♂️
Element のような単語でもいい

本来は抽象化するためのものであまり具体的なことはわからないので、意味のない一文字を使うのが基本

T1, T2, T3, T4 みたいになってしまうと複雑なので名前をつけることもある
Rust を参照