ジェネリクスって何?
手始めに日本語ガイド?「ジェネリクス」を見たが流石に内容が薄かったので、本文中にあった「The Java Tutorial」を参考にした。が、英語苦手勢にはやる気がおきなかったため拾ったジェネリクス導入の歴史をから書いてある 「ジェネリクスについて ジェネリクスによる型保証と可読性の向上」 をメインに読み進め、詰まったら「The Java Tutorial」を読んでみるスタイルで進めた。ほぼこの「new to Java」のタイトルごとにメモ書きを書いた。
資料
- メインで読み進める方
- 困ったとき参考にする
結論
<T>
で囲ったものをジェネリクスと呼ぶ。中にかかれたT
はたまたまでAでもBでも何でも良い(一応命名の推奨ルールはある)。
Javaは型保証を売りにしているが、例えばListを使う際どんな型(List<String>とか、List<Int>とか)が指定されるかわからない。そのためジェネリクス <T>
で仮置きし、コンパイル時に指定された型を伝えている。型の仮置きがジェネリクスだと思われる。
new to Javeを読み進める
型の損失(Type Loss)と型パラメータ
前半は型保証導入の背景話。
Listはどんな型でも持てるようにObjectを受け取るようにしていたため、取り出す際はキャストが必要だしそもそも何でも入れれちゃうのでどれでキャストしてよいかもよくわからんかった。しかも、動かして値を入れてからじゃないとCastExceptionで落ちるか動かもわからんかった。型保証が売りのjavaが型保証できてないのでこれをやめたい!
コンパイラに型を教えたいので一旦定義を<E>(型パラメータ)
としておいて、使う際に<String>
とかとか指定する作戦にしたらしい。これをジェネリクスと呼ぶ(?)たぶん。見事解決!
でこの型パラメータEは抽象度を上げてくれるもので、型の抽象化という強力な武器をJavaは手に入れたらしい。理解が浅いので、、今のとここんなとき素敵に動くとかの例は考えられなかったw
いったんクソみたいなアウトプットをしてみる。
myInt.add("aaa")
はInteger
では無いため実行前にちゃんと怒られることを確認した
public class Main {
public static void main(String[] args) {
// ジェネリクスについて ジェネリクスによる形保証と可読性の向上
MyListWithSizeOnlyOne<String> myListWithSizeOnlyOne = new MyListWithSizeOnlyOne<String>();
myListWithSizeOnlyOne.add("AAAA");
System.out.println(myListWithSizeOnlyOne.get());
MyListWithSizeOnlyOne<Integer> myInt = new MyListWithSizeOnlyOne<Integer>();
myInt.add("aaa"); // IntegerにString入れようとしてるのでコンパイルエラーになる
System.out.println(myInt.get());
}
}
class MyListWithSizeOnlyOne<E> {
private E Element;
public void add(E element){
this.Element = element;
}
public E get(){
return Element;
}
}
サブタイピング
クラスの関係性(サブタイピング)はList<class名>では別の関係性になってるよって話。 Personインタフェースとこれを継承するStudent,Teacherクラスを作成した場合、StudentはPersonのサブクラスだがList<Student>はList<Person>のサブクラスでは無いらしい。でこの問題(?)は次でやるってことで問題提起だけされてた。 でクソみたいなアウトプットを…。問題通りPersonインタフェースとこれを継承するStudentクラスを作成。Personインタフェースを受取内容を表示する関数にStudentクラス渡して怒られることを確認した。
Personインタフェース
public interface Person {
String getFirstName();
}
Student
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class Student implements Person{
private String FirstName;
@Override
public String getFirstName() {
return FirstName;
}
}
まー確かに型ちがうからprintList(studentList);
で怒られることだけ確認した。
public class Main {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
Student student = new Student("Tom");
studentList.add(student);
printList(studentList); //List<Person>にList<Student>入れてるのでコンパイルエラー
}
private static void printList(List<Person> list) {
list.forEach(person -> System.out.println(person.getFirstName()));
}
}
ワイルドカード>(unknown型)
なんでListはクラスの関係性(サブタイプ)が消えるか。
classではStudentがPersonのサブタイプであったとしても、ListではList<Person>とList<Student>の関係性(サブタイプ)が消えてしまう。そもそもなんでサブタイプの関係性が消えてしまうのか?Listでもクラスの関係性(サブタイプ)が消えない世界線において例えばPersonのサブタイプTeacherを作った場合、下記のようなList<Person>を受け取りteacherを追加するようなメソッドがあったとする。一見良さそうだがPersonのサブタイプであるStudentを渡した場合、StudentにTeacherを追加することになってしまう。これを防止するためListは関係性を一切持たせないようにしたらしい。
public void add(List<Person> list) {
Teacher teacher = new Teacher();
list.add(teacher)
}
でも汎用的なメソッドを作れないから困る…ので何でも受け取れるワイルドカード(unknown型)が提供されてる。全てのListはこのUnknown型(List>)のサブタイプと定義されているため、どんなListでも渡せることができる。正しPersonとTeacherのようなクラス関係は消え、あくまでもUnknown型のサブタイプとみなされるためPersonやStudent固有の操作はできない。上記コードももちろん動かない(どんなListでも受け取れるがそのリストはUnknown型のサブタイプとみなされているため、知らないTeacher型は追加できない)
public void add(List<?> list) {
Teacher teacher = new Teacher();
list.add(teacher) // コンパイルエラーで怒られる
}
// Listの操作なので怒られない
private void printSize(List<?> list) {
System.out.println(list.size());
}
境界付きワイルドカード List<? extends Person>
ワイルドカードでどんなListも受け取れるようになったがPerson固有の操作ができない…これを解決するためにワイルドカードにextendsを使用して境界をつけれるらしい。 クラスにおけるPersonとStudentの関係性を表したようにワイルドカードとPersonの関係性をextendsを使用して表現できる。表現方法がクラスと同じなのでわかりやすいな。
クソみたいなアウトプットを。。
public class Main {
public static void main(String[] args) {
List<Student> studentList = new ArrayList<>();
Student student = new Student("Tom");
studentList.add(student);
printList(studentList); //List<Person>にList<Student>入れてるのでコンパイルエラー
printFirstName(studentList); //実行できる!
}
private static void printList(List<Person> list) {
list.forEach(person -> System.out.println(person.getFirstName()));
}
private static void printFirstName(List<? extends Person> list) {
list.forEach(person -> System.out.println(person.getFirstName()));
}
}
この境界付きワイルドカードはListに限らず使用可能、例えばクラスとかにも使用できるらしいが、いまいち嬉しさがわからなかった。。ま、Classにも使えますが、どんな用途で使うのか例はまったく思いつかなかった。。。そして突然<T>
がでてきたが<E>(型パラメータ)
と何を区別したくて変えてるのかは書いてなかった。意味は同じだと思うので気にしないことにする。
public class MyNumber <T extends Person>{
private final T person;
public MyNumber(T person) {
this.person = person;
}
}
そしてジェネリクスはメソッドにも使える。紹介されてる例がどうやってメソッドを使うかとか書いて無くて微妙だが、ORACLEのThe Java Tutorialsに詳しく書いてあったのでそっちを参考にした。
型削除
Listはどんな型でも持てるようにObjectを受け取るようにしていたため、コンパイル時にどんな型をしていしたか伝えるため<E>(型パラメータ)
を導入した。この型パラメータはコンパイル時まで型情報を渡すが、以降の実行ファイルとかメモリに乗ったやつとかは型情報が削除されObjectとして持つ。それにまつわる利点や注意点が書いてあった。読み物としてフムフムってぐらいで読めた。
その他
<E>(型パラメータ)
とか<T>
とか使い分けの指標がThe Java Tutorialsにあった。
E - Element (used extensively by the Java Collections Framework)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types