Share
Embed
Java ジェネリクス入門 2024
JJUG CCC 2024 Fall で発表したジェネリクス入門セッションの資料です。
ジェネリクスの基本的な構文と、変性について解説し、Stream API のjavadocが雰囲気で理解できるようになるぐらいを目標にしています。
nagise
October 27, 2024
Transcript
60 slides
-
富⼭県出⾝ 富⼭市在住 株式会社 クレディセゾン 株式会社 凪瀬アーキテクツ 主なジャンル •Java⾔語仕様 ◦ https://docs.oracle.com/javase/specs/index.html
•書籍 Effective Java の解説ポッドキャスト Tsundokanai Radio ◦ https://podcasters.spotify.com/pod/show/dokanai/ •Javaのジェネリクス •⽇本の旧暦のライブラリ(開発中) •Blog : プログラマーの脳みそ •Burikaigi (富⼭で1⽉末~2⽉頭で⾏われるカンファレンス) ⾃⼰紹介 なぎせゆうき X (Twitter) @nagise -
• 1973年 ML⾔語 によってジェネリックプログラミングが開拓 • 1977年 Ada に搭載 • 1993年
C++ の STL (Standard Template Library) がANSI/ISO委員会に提出 • 1995年 Java 登場 ◦ 当初から検討はされていたがリリースに間に合わず⾒送り • 2002年 五⼗嵐淳 On Variance-Based Subtyping for Parametric Types • 2003年 Scala 登場 • 2004年 Java 5 にてジェネリクスが搭載 • 2005年 C# バージョン 2.0 でジェネリクス搭載 • 2011年 Kotlin 登場 • 2011年 Java 7 ダイヤモンド演算⼦が追加 • 2014年 Java 8 ラムダ式が追加 • 2018年 Java 10 varによる変数宣⾔ • 20XX年 Project Valhalla ? ジェネリクス関連の歴史トピックス -
List<String> list = new ArrayList<String>(); list.add("甲"); list.add("⼄"); String s0 =
list.get(0); Listのメソッドの定義 boolean add(E e) E get(int index) ジェネリクスの使⽤例 -
List list = new ArrayList(); list.add("⼦"); list.add("丑"); Integer i =
(Integer) list.get(0); //キャスト必須 実⾏時にClassCastException コンパイル時点でヒューマンエラー を検出できない ジェネリクスは単なるキャストのシンタックスシュガーではない 古代のJavaのコード(〜2004年 Java 5 より前) -
Java 5 より前の時代のソースコードもコンパイルできるように するため List list = new ArrayList(); ただし、現代のIDEだとおおよそ警告が出るハズ
基本的に避けるべき §4.8. Raw Types raw型 -
型変数 (§4.4. Type Variables) 型そのものを変数に⼊れて扱う ListのjavadocではList<T> 型変数名は慣習として アルファベット1⽂字が多い あるいは⼤⽂字スネークケース 標準APIでも
java.util にPrimitiveIterator<T, T_CONS> といった利⽤例もある。⾔語仕様的には⽇本語名でも可(§3.8. Identifiers) 型変数 -
java.util.Collections クラスより リスト内に出現する指定された値を すべてほかの値に置き換えます。 public static <T> boolean replaceAll (List<T>
list, T oldVal, T newVal) 3つの引数がList<T>型、 T型、 T型という関係性 メソッドスコープのジェネリクス -
java.util.Collections クラスより 指定されたオブジェクトだけを格納している 不変のセットを返します public static <T> Set<T> singleton(T o)
引数がT型で、戻り値がSet<T>型という関係性 メソッドスコープのジェネリクス -
• 複数のメソッド間の引数・戻り値の 型の関係性 • 公開されているフィールド • 内部クラス new するタイミングで型変数の具体的 な型を指定する(型変数のバインド)
インスタンス単位で別のバインド new ArrayList<String>(); new ArrayList<Integer>(); インスタンススコープのジェネリクス -
ArrayList の iterator() はエンクロージング内部クラス Itr を返す public Iterator<E> iterator() {
return new Itr(); } 内部クラス Itr の宣⾔はこんな感じ。ArrayListの <E> を Iterator<E> としてバインドして使っている public class ArrayList<E> /** いろいろ略 */ { private class Itr implements Iterator<E> { … } } インスタンスに紐づく内部クラスへの波及 ArrayList<String> Itr implements <String> 発展的内容 -
public class Syntax<T> implements Iterable<String> { public <X> void hoge(X
x) { List<X> list = new ArrayList<X>(); list.add(x); } @Override public Iterator<String> iterator() { return null; } } 似て非なる<>を色分けしてみました ⽂法の話 -
型変数の宣⾔ class Hoge<T>{} // インスタンススコープ public <T> void hoge() {}
// メソッドスコープ 型変数のバインド new ArrayList<String>(); // new でのバインド class StringList extends ArrayList<String> {} // 継承でのバインド Collections.<String> replaceAll(list, "hoge", "piyo"); // メソッドでのバインド パラメータ化された型 (parameterized type) List<String> list; 3種の<> -
ジェネリッククラスの例 java.util.ArrayList より 宣⾔側 public class ArrayList<E> /*略*/ {} 利⽤側でバインド
List<String> list = new ArrayList<String>(); 推論 List<String> list = new ArrayList<>(); 型変数の宣⾔とバインド -
ジェネリックメソッドの例 java.util.Collections より 宣⾔側 public static <T> boolean replaceAll (List<T>
list, T oldVal, T newVal) 利⽤側 Collections.<String>replaceAll(list, "hoge", "piyo"); 推論 Collections.replaceAll(list, "hoge", "piyo"); 型変数の宣⾔とバインド -
ジェネリックメソッド、複数型変数の例 java.util.Collections より 宣⾔側 public static <K, V> Map<K, V>
singletonMap(K key, V value) 利⽤側 Collections.<String, Integer>singletonMap("hoge", 123); 推論 Collections.singletonMap("hoge", 123); 型変数の宣⾔とバインド -
インスタンススコープとメソッドスコープ併⽤の例 java.util.Optional<T> 宣⾔側 public <U> Optional<T> map(Function<? super T, ?
extends U> mapper) 利⽤側 Optional<Integer> optI = Optional.<Integer>of(123); Optional<String> optS = optI.<String>map( (Integer i) -> i.toString()); 推論 var optI = Optional.of(123); var optS = optI.map( (i) -> i.toString()); 型変数の宣⾔とバインド 発展的内容 -
「パラメータ化された型」§4.5. Parameterized Types 変数宣⾔時の型で出てくる<> List<String> stringList = List.of("甲","⼄","丙"); パラメタライズドタイプではワイルドカードが使える List<?>
someList; List<? extends Hoge> extendsList; List<? super Hoge> superList; 詳細は後ほど パラメタライズドタイプ -
T 型のオブジェクト x に関して真となる属性を q(x) とする。 このとき S が T
の派⽣型であれば、 S 型のオブジェクト y について q(y) が真となる。 端的に⾔えば、親クラスは⼦クラスで代替できる。 ⼦クラスは親クラスの機能を全て代替できなけれ ばならない。 リスコフの置換原則 計算機科学博⼠ バーバラ・リスコフ 1939年〜 -
Javaは変数の代⼊は共変(covariant)なので Integer integer = Integer.valueOf(123456); Number number = integer; といったように、親の型の変数に⼦の型の変数を代⼊することができる
逆は無条件ではできなくて明⽰的なキャストが必要、 かつ実⾏時例外も出る Number number = Integer.valueOf(123456); Integer integer = (Integer) number;// 常に成功するとは⾔えない Javaの変数は基本的に共変 -
このパターンはOK ArrayList<String> arrayList = new ArrayList<>(); List<String> list = arrayList;
このパターンはNG List<Integer> integerList = List.of(123); List<Number> numberList = integerList; // NG パラメタライズドタイプと共変・⾮変 なんでだろう? 型の不⼀致: List<Integer> から List<Number> には変換できません -
パラメタライズドタイプの型変数部分が共変だとダメな例 飲⽤ボトル<粉ミルク> 哺乳瓶 = new 飲⽤ボトル<粉ミルク>(); 飲⽤ボトル<飲料> ボトル = 哺乳瓶;
// 実際はNG ボトル.add (new ウイスキー()); パラメタライズドタイプと共変・⾮変 -
このパターンはOK 型識別⼦部分は共変(covariant) ArrayList<String> arrayList = new ArrayList<>(); List<String> list =
arrayList; このパターンはNG 型変数部分は⾮変(invariant) List<Integer> integerList = List.of(123); List<Number> numberList = integerList; // NG §4.10. Subtyping パラメタライズドタイプと共変・⾮変 -
Javaの配列は共変 粉ミルク[] 粉ミルクストック = new 粉ミルク[] {”はいはい”, ”つよいこ”} 飲料[] 飲料ストック
= 粉ミルクストック; // コンパイルが通る 飲料ストック[0] = ”⼭崎”; コンパイルエラーは出ないが、実⾏時にはArrayStoreException 実⾏時に型を確認するアプローチ 配列の共変 -
public void のどを潤す(Supplier<飲料> ⾃動販売機, Consumer<飲料> ⼈) { 飲料 drink =
⾃動販売機.get(); ⼈.accept (drink); } パラメタライズドタイプで共変を使いたいケース Supplier<飲料> Consumer<飲料> -
Supplier<飲料> ⾃動販売機A; Supplier<コーラ> ⾃動販売機B; のどを潤す(⾃動販売機A, ⼈); // OK のどを潤す(⾃動販売機B, ⼈);
// NG パラメタライズドタイプで共変を使いたいケース Supplier<飲料> Supplier<コーラ> コーラだけの⾃販 機でも別にいいん だけどなあ…… -
普通の⾃動販売機A 飲料 get() void 補充(飲料) コーラ専⽤の⾃動販売機B コーラ get() void 補充(コーラ)
共変にできない理由を振り返る リスコフの置 換原則違反! でもそこ使わ ないじゃん B extends A -
<? extends 飲料> を表す型 コンパイル時の型の推論にだけ現れ、単体で宣⾔することはできない §5.1.10. Capture Conversion Capture というトリック
Capture <? extends 飲料> キャスト可 飲料 キャプチャ変換 <飲料> <コーラ> ※いろいろと⽅便です -
List<? extends 飲料> 飲み物リスト 飲料 x = 飲み物リスト.getFirst(); // OK
飲み物リスト.add(コーラ); // NG! Capture というトリック 出⼒(戻り値)にだけ使えて ⼊⼒(引数)には使えない Capture <? extends 飲料> キャスト可 飲料 キャスト不可 飲料 -
public void のどを潤す(Supplier<? extends 飲料> ⾃動販売機, Consumer<飲料> ⼈) { 飲料
drink = ⾃動販売機.get(); // Capture<? extends 飲料> から 飲料 へのキャスト ⼈.accept (drink); } Captureで共変 Consumer<飲料> Supplier<飲料> Supplier<コーラ> どっちも OK! -
List<? super 飲料> 飲み物リスト 飲料 x = 飲み物リスト.getFirst(); // NG!
飲み物リスト.add(コーラ); // OK! 反変のCapture ⼊⼒(引数)にだけ使えて 出⼒(戻り値)には使えない 何に使うの? Capture <? super 飲料> キャストはObjectにだけ Object <飲料> <Object> キャプチャ変換 -
public void 消費する(Supplier<飲料> ⾃動販売機, Consumer<? super 飲料> 消費者) { 飲み物
drink = ⾃動販売機.get(); 消費者.accept (drink); } 反変で消費する Supplier<飲料> Consumer<飲料> 飲み物が消費できればなんでも可 Consumer<Object> -
java.util.stream.Stream のメソッドには共変・反変のが多⽤さ れている mapに渡す関数は 「T を渡すと R を返す」 ことが出来る必要がある ⼊⼒はTより広くても良いし
出⼒はRより具体的でも良い Stream API のドキュメント -
• Javaの型推論四天王 • 型変数の境界 • 再帰ジェネリクス • イレイジャ • ジェネリクスとリフレクション
• 未来のジェネリクス • チューリング完全 おまけ -
• Java 5 : ジェネリックメソッドの型推論 • Java 7 : ダイヤモンド演算⼦
• Java 8 : ラムダ式の引数の型推論 • Java 10 : ローカル変数宣⾔の型推論 var Javaの⾔語仕様的には型推論が⽣じるところが複数ある • Java21 : Record Pattern の型推論 new! (JEP 440) Javaの型推論四天王 -
public class Hoge<T extends X> 型変数を宣⾔する際に型変数Tの上限境界を指定することが出来る。 ここでは extends は使えるが super
は使えない。 パラメタライズドタイプのワイルドカードと紛らわしい public class Hoge<T extends X & Cloneable> & を⽤いて interface を implements していることを表すこともできる 型変数の境界 -
§4.6. Type Erasure イレイジャとは、型(パラメータ化された型や型変数を含む可能性があ る)から型(パラメータ化された型や型変数ではない)への写像である。 型Tのイレイジャを|T|と書く ⼤雑把に⾔えば List<String> → |List|
Map<String, Integer> → |Map| イレイジャとは何か -
⼤雑把には • 同じクラス • 同じ名前 • 引数のイレイジャが同じ オーバーライド等価という概念 override-equivalent (§8.4.2.
Method Signature) void hoge(List<String> list) void hoge(List<Integer> list) はイレイジャが同じ |List| になるので共存できない Javaのメソッド シグネチャ -
イレイジャの誤解で実⾏時にはなんでも消えるイメージがあるがそうで もない public static <T extends Number> void hoge(T t)
メソッドをコンパイルしてclassファイルを作り、 classpathにこのclassを置いて、hogeメソッドを利⽤しようとすれば TがNumberでなければコンパイルエラーになる ジェネリクスとリフレクション classファイル <T extends Number> <T extends Number> -
ジェネリクス関連のJEPはいくつか存在している • JEP 218: Generics over Primitive Types (2014 /
2017 updated) • JEP 300: Augment Use-Site Variance with Declaration-Site Defaults (2014 / 2016 updated) • JEP 402: Enhanced Primitive Boxing (Preview) (2021 / 2024 updated) • JEP draft 8261529: Universal Generics (Preview) (2021 / 2023 updated) • JEP draft 8303099: Null-Restricted and Nullable Types (Preview) (2023 / 2024 updated) 未来のジェネリクス -
2014年の古株のJEP Project Valhalla の管轄 プリミティブ型をジェネリクスで扱えるようにする 内容としては JEP 402 と被っていて 402
で進められていそう JEP 218: Generics over Primitive Types -
更新が⽌まっているので活きているのかどうなのか Java の型変数は 利⽤側で変性を宣⾔する⽅式(use-site variance annotations) C#やKotlinは型変数の宣⾔側で変性を指定(declaration-site variance annotations) 宣⾔側でデフォルトの変性を指定できるようにしようという試み
JEP 300: Augment Use-Site Variance with Declaration-Site Defaults -
Java / Java VM のリファクタリングをする巨⼤プロジェクト Value Type を導⼊してVMの効率をアップする⽬論み 既存のものも含め Value
Type / 参照型 に整理 そのどちらでも同様にジェネリクスを適⽤ Codes like a class, works like an int. Project Valhalla -
Valhalla 関連 JEP Valhallaで導⼊予定のValue Typeもジェネリクスで⽤いること が出来るように。 Valhalla⾃体が検証中であるため、⽅針が⼆転三転。 最近、ステータスが Closed /
Withdrawn になった。 後述のJEPに置き換わった模様 JEP draft: Universal Generics (Preview) -
Valhalla 関連 JEP プリミティブ型を参照型っぽく使えるように。 この⼀環としてプリミティブ型を型変数に⽤いることができるよ うにする。 JEP 402: Enhanced Primitive
Boxing (Preview) int i = 12; int iSize = i.SIZE; double iAsDouble = i.doubleValue(); Supplier<String> iSupp = i::toString; -
Valhalla 関連 JEP Javaの参照型を強化し、nullがある型かどうか表現できるように する。 型変数にもnull許容 / null制限 を表現できるように拡張する •
T ! - null 制限型 • T ? - null 許容型 • Box<String!> - null制限型のパラメタライズドタイプ • Box<String?> - null許容型のパラメタライズドタイプ JEP draft: Null-Restricted and Nullable Types (Preview) -
Radu Grigore (2016). Java Generics are Turing Complete 絶対にIDEに貼り付けてはいけない。 絶対にバージョン管理システムに
コミットしてはいけない。 絶対にCIを回してはいけない。 Javaのジェネリクスはチューリング完全? コードなコピペそ -
l Java⾔語仕様 l The Java® Language Specification Java SE 21
Edition https://docs.oracle.com/javase/specs/jls/se21/html/ l Java標準APIのjavadoc l Java® Platform, Standard Edition & Java Development Kit バージョン21 API仕様 https://docs.oracle.com/javase/jp/21/docs/api/ l JEP 各種 l https://openjdk.org/jeps/0 l Project Valhalla l https://openjdk.org/projects/valhalla/ l Radu Grigore (2016). Java Generics are Turing Complete l 五⼗嵐淳 (2002). On Variance-Based Subtyping for Parametric Types l @nagiseの過去の資料 l Project Valhalla 2023 https://nagise.hatenablog.jp/entry/2023/04/17/210648 l Java Generics Hell Advent Calendar 2017 https://adventar.org/calendars/2751 l OpenJDK の duke ギャラリー l https://wiki.openjdk.org/display/duke/Gallery l いらすとや l https://www.irasutoya.com/ 参考⽂献リスト Duke Duchess