>100 Views
May 29, 26
スライド概要
JJUG CCC 2026 Springのセッション『なぜJavaのジェネリクスには 「PECS原則」が必要なのか?~Scala/Kotlinと⽐較して理解するワイルドカードの設計思想~』の公開資料です。
シンプレクスは1997年の創業以来、メガバンクや大手総合証券を筆頭に、日本を代表する金融機関のテクノロジーパートナーとしてビジネスを展開してきました。現在では、金融領域で培った豊富なノウハウを活用し、金融機関以外の領域でもソリューションを展開しています。2019年3月にはAI企業のDeep Percept株式会社、2021年4月には総合コンサルティングファームのXspear Consulting株式会社がグループに加わり、創業時より付加価値の創造に取り組んできたシンプレクスとワンチームとなって、公的機関や金融機関、各業界をリードする企業のデジタルトランスフォーメーション(DX)の推進を支援しています。
なぜJavaのジェネリクスには 「PECS原則」が必要なのか? Scala/Kotlinと比較して理解するワイルドカードの設計思想 シンプレクス株式会社 山田 耀平 © 2026 Simplex Inc. 1
自己紹介 山田 耀平 2023年シンプレクス入社 主にFX取引システムのバックエンド開発に関わる 言語やフレームワークの仕様・特徴を調べることが好き © 2026 Simplex Inc. 2
はじめに 生成AIやコーディングエージェントによって、開発にとどまらずレビューすら自動化されつつある中 ジェネリクスの読み方・書き方 を知ることがどう役に立つのか 個人的な考え 「曖昧さの少なさ」では、形式言語の方が自然言語より依然として優れている 型やメソッドシグネチャは、設計意図や役割を簡潔に伝えられる 人間とAIが設計意図を共有するうえでの共通言語としての役割 © 2026 Simplex Inc. 3
シグネチャから意図を読む void copy(List<? super T> dest, List<? extends T> src) dest は T を受け取れる変数 src は T を取り出せる変数 T が src から dest へ流れる 今日のゴール このシグネチャの ? super T と ? extends T が理解できるようになる Kotlin/Scalaでは同じことを表現するとどうなるのかを見る なぜPECSという原則が必要になるのか考える © 2026 Simplex Inc. 4
Javaのジェネリクス: 基本おさらい 型引数を使って、型をパラメータ化する コンパイル時に型チェックできる 利用側のキャストを減らせる List<String> names = new ArrayList<>(); names.add("Alice"); String name = names.get(0); // キャスト不要 © 2026 Simplex Inc. 5
型引数は「実行時の型」ではない Javaのジェネリクスは erasure により実装されている 型引数は主にコンパイル時の検査に使われ、実行時には除去(erase)される 実行時に List<String> と List<Integer> が同じクラスとして扱われる List<String> strings = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); System.out.println(strings.getClass() == integers.getClass()); // true © 2026 Simplex Inc. 6
重要: List<String> は List<Object> ではない String string = "Bob"; Object object = string; // エラーにはならない List<String> strings = new ArrayList<>(); List<Object> objects = strings; // コンパイルエラー String は Object のサブタイプ しかし List<String> は List<Object> のサブタイプではない © 2026 Simplex Inc. 7
もし List<String> を List<Object> として扱えたら List<String> strings = new ArrayList<>(); List<Object> objects = strings; // もしOKだったら objects.add(123); String s = strings.get(0); // Stringとして読めなくなる List<Object> は Object であれば何でも(当然 Integer も)追加できる List<String> には Integer は追加できない → List<String> は List<Object> として扱うことができない © 2026 Simplex Inc. 8
Javaで型の幅を広げたい場面
double sum(List<Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
List<Integer> ints = List.of(1, 2, 3);
sum(ints); // コンパイルエラー
sum はリストから値を読むだけ
Integer は Number として扱える
しかし List<Integer> は List<Number> として扱うことができない
© 2026 Simplex Inc.
9
ワイルドカードを用いて扱う型の幅を広げる
double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
List<Integer> ints = List.of(1, 2, 3);
sum(ints); // OK
? extends Number
「要素型は Number のサブタイプの何か」
取り出した値は Number として扱える
© 2026 Simplex Inc.
10
? extends T の読み方 List<? extends Number> xs; List<Integer> かもしれない List<Double> かもしれない List<BigDecimal> かもしれない 少なくとも、取り出した値は Number として扱える 上限境界: unknown type extends Number © 2026 Simplex Inc. 11
? extends T はProducer List<? extends Number> xs = new ArrayList<Integer>(List.of(1, 2, 3)); Number n = xs.get(0); // OK xs.add(1); // NG xs は Number を生産する つまり、取り出す側から見るとProducer ただし、実体は List<Double> かもしれない 具体的な要素型は不明なので追加できない © 2026 Simplex Inc. 12
書き込み先の型の幅も広げたい void addSamples(List<Integer> dest) { dest.add(1); dest.add(2); } List<Number> numbers = new ArrayList<>(); addSamples(numbers); // コンパイルエラー List<Number> には Integer を追加できる しかし List<Integer> としては受け取れない © 2026 Simplex Inc. 13
そこでワイルドカード void addSamples(List<? super Integer> dest) { dest.add(1); dest.add(2); } List<Number> numbers = new ArrayList<>(); addSamples(numbers); // OK ? super Integer 「要素型は Integer のスーパータイプの何か」 Integer は安全に書き込める © 2026 Simplex Inc. 14
? super T の読み方 List<? super Integer> xs; List<Integer> かもしれない List<Number> かもしれない List<Object> かもしれない 少なくとも、 Integer は安全に追加できる 下限境界: unknown type super Integer © 2026 Simplex Inc. 15
? super T はConsumer List<? super Integer> xs = new ArrayList<Number>(); xs.add(1); // OK Object value = xs.get(0); // OK Integer i = xs.get(0); // NG xs は Integer を消費できる つまり、入れる側から見るとConsumer 実体は List<Object> かもしれない 具体的な要素型は不明なので、読むときは Object まで広がる © 2026 Simplex Inc. 16
PECS原則 Producer Extends, Consumer Super 役割 使う型 できること できないこと Producer ? extends T T として読む T を書く Consumer ? super T T を書く T として読む 読み書き両方なら、ワイルドカードではなく T © 2026 Simplex Inc. 17
標準API: Collections.copy
public static <T> void copy(
List<? super T> dest,
List<? extends T> src) {
...
}
List<? extends T> src は値を取り出すProducer
List<? super T> dest は値を受け取るConsumer
© 2026 Simplex Inc.
18
Kotlin/Scalaとの比較の前に Javaではワイルドカードを使って受け入れ可能な型の幅を広げることができる 背景にある考え方が variance(変性) A <: B (例えば Integer <: Number )のとき: 用語 関係 Javaで対応する考え方(※) 共変 F<A> <: F<B> List<Integer> <: List<? extends Number> 反変 F<B> <: F<A> List<Number> <: List<? super Integer> 不変 どちらでもない List<Integer> と List<Number> は別物 ※Javaの List<T> 自体が共変/反変になるわけではないので、あくまでイメージ © 2026 Simplex Inc. 19
Kotlinとの比較
Kotlinでは、型パラメータのvarianceを 型の宣言側 で指定できる
この方式を declaration-site variance と呼ぶ
class Box<out T>(val value: T)
interface Serializer<in T> {
fun serialize(value: T): String
}
out T : 共変
in T : 反変
何も付けない: 不変
© 2026 Simplex Inc.
20
Kotlin: out / in の例 val strings: Box<String> = Box("hello") val chars: Box<CharSequence> = strings // OK val anySerializer: Serializer<Any> = serializerForAny() val stringSerializer: Serializer<String> = anySerializer // OK Box<String> は Box<CharSequence> として扱える Serializer<Any> は Serializer<String> として扱える Javaの ? extends T / ? super T に近い © 2026 Simplex Inc. 21
Kotlin: 位置制約 out T は、引数位置にそのまま置けない interface BadBox<out T> { fun get(): T fun put(value: T) // コンパイルエラー } out T はProducerとして使う型 put(value: T) はConsumerとして使っているためコンパイルエラー in T では逆に、戻り値位置にそのまま置けない PECS原則を言語仕様として自然にサポート © 2026 Simplex Inc. 22
Kotlin標準ライブラリで見る interface List<out E> : Collection<E> interface MutableList<E> : List<E>, MutableCollection<E> Kotlinの List は読み取り専用ビューであり、共変 追加・更新できる MutableList は不変 © 2026 Simplex Inc. 23
Kotlin: Type projection
Kotlinでは、Javaと同じようにジェネリクスの 使用時に out/in を指定 することもできる
この方式を use-site variance と呼ぶ
fun total(xs: MutableList<out Number>): Double {
return xs.sumOf { it.toDouble() }
}
fun addSamples(dest: MutableList<in Int>) {
dest.add(1)
dest.add(2)
}
out Number : Number として読み出せる
in Int : Int を書き込める
© 2026 Simplex Inc.
24
Scalaとの比較 Scalaでも、宣言側でvarianceを指定できる(declaration-site variance) final class Box[+A](val value: A) trait Serializer[-A] { def serialize(value: A): String } +A : 共変 -A : 反変 何も付けない: 不変 © 2026 Simplex Inc. 25
Scala: 共変と反変の例 val strings: Box[String] = new Box("hello") val chars: Box[CharSequence] = strings // OK val anySerializer: Serializer[Any] = ??? val stringSerializer: Serializer[String] = anySerializer // OK Box[String] は Box[CharSequence] として扱える Serializer[Any] は Serializer[String] として扱える © 2026 Simplex Inc. 26
Scala: 型境界 Scalaにも、Javaの ? extends / ? super に相当する表現がある ? <: T // 上限境界 ? >: T // 下限境界 ? <: T : 上限境界で、Javaの ? extends T に近い ? >: T : 下限境界で、Javaの ? super T に近い ※ Scala 3のワイルドカード記法で、Scala 2では _ <: T / _ >: T © 2026 Simplex Inc. 27
PECSはどこに現れるか 言語 主な表現場所 補助的な表現 Java 利用箇所の ? extends / ? super - Kotlin 宣言側の out T / in T 利用箇所の out T / in T Scala 宣言側の +T / -T 利用箇所の ? <: T / ? >: T Kotlin/Scalaでは、PECS的な制約が型宣言に組み込まれやすい Javaでは、API設計者が利用箇所のワイルドカードとして表現する どちらも同じ型安全性の問題を、違う場所で表現している © 2026 Simplex Inc. 28
なぜJavaはuse-site varianceだけなのか Java Genericsは、既存の非ジェネリックなCollections APIとの互換性を保って導入された 既存のコレクションAPIを壊さずにジェネリクス化する必要があった List<E> は読み書きの両方を持つため、型そのものは不変にする Producer/Consumerの役割は、メソッドごとのワイルドカードで表す PECSは、API設計者がその役割を選ぶための指針 © 2026 Simplex Inc. 29
クロージング Javaのジェネリクスは不変が基本 ? extends はProducerを柔軟に受け取るための仕組み ? super はConsumerを柔軟に受け取るための仕組み PECSはJavaのuse-site varianceを使いこなすための読み方 Kotlin/Scalaと比べると、Javaは「役割を利用箇所に書く」設計 © 2026 Simplex Inc. 30
アンケート セッションアンケート © 2026 Simplex Inc. 全体アンケート 31