はじめに
ジェネリクスの「変性(variance)」を理解するとき、
Java ではしばしば PECS 原則 が引用されます。
これは「共変 (out)」「反変 (in)」を直感的に理解する助けになります。
1. PECS 原則とは?
PECS = Producer Extends, Consumer Super
Java のワイルドカード型(? extends T, ? super T)を理解するための経験則であり、
Kotlin の out / in にもそのまま対応します。
| 英語 | 意味 | Kotlin の対応 |
|---|---|---|
| Producer Extends | 値を「生み出す(出す)」型は extends
|
out |
| Consumer Super | 値を「消費する(受け取る)」型は super
|
in |
つまり:
-
出力側 (Producer) → 共変 (
out) -
入力側 (Consumer) → 反変 (
in)
2. 例:Producer(出力)
データを「生み出す」クラスは 共変 (out) にすべきです。
class Producer<out T>(private val value: T) {
fun produce(): T = value
}
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer // OK(共変)
println(anyProducer.produce()) // Hello
Producer<String> は「String を生み出す」だけなので、
「Any を生み出す」として扱っても安全です。
Java での同義語:
Producer<? extends Any>
3. 例:Consumer(入力)
データを「受け取る」クラスは 反変 (in) にすべきです。
class Consumer<in T> {
fun consume(value: T) {
println("Consumed: $value")
}
}
val numberConsumer: Consumer<Number> = Consumer()
val intConsumer: Consumer<Int> = numberConsumer // OK(反変)
intConsumer.consume(42) // Consumed: 42
Consumer<Number> は「Number(親型)」を受け取れるので、
Consumer<Int>(子型)として扱っても問題ありません。
Java での同義語:
Consumer<? super Int>
4. PECS をイメージで理解する
Producer → out → 出すだけ(読み取り専用)
Consumer → in → 入れるだけ(書き込み専用)
┌───────────────────────────┐
│ Producer (out) │
│ ┌──────────────┐ │
│ │ produce():T │───▶ │
│ └──────────────┘ │
└───────────────────────────┘
┌───────────────────────────┐
│ Consumer (in) │
│ ◀─── consume(value:T) │
└───────────────────────────┘
5. 関数型にも当てはまる
Kotlin の関数型 (A) -> B も、PECS の原則そのものです。
| 位置 | 役割 | 変性 | キーワード |
|---|---|---|---|
| 引数 (A) | 消費者 (Consumer) | 反変 | in |
| 戻り値 (B) | 生産者 (Producer) | 共変 | out |
val f: (Number) -> String = { "Number: $it" }
val g: (Int) -> Any = f // OK
引数(in)は Number → Int の方向(反変)
戻り値(out)は String → Any の方向(共変)
6. まとめ:PECS のルールを Kotlin に置き換えると
| 役割 | Java 記法 | Kotlin 記法 | 説明 |
|---|---|---|---|
| Producer(出力) | ? extends T |
out T |
出すだけ。共変。読み取り専用。 |
| Consumer(入力) | ? super T |
in T |
入れるだけ。反変。書き込み専用。 |
覚え方:
PECS = "Producer Extends, Consumer Super"
Kotlin では “Producer out, Consumer in”
7. 実践例:共変+反変の組み合わせ
interface Transformer<in I, out O> {
fun transform(input: I): O
}
class StringLengthTransformer : Transformer<String, Int> {
override fun transform(input: String): Int = input.length
}
val t: Transformer<CharSequence, Number> = StringLengthTransformer()
println(t.transform("Hello")) // 5
I(入力)は in(反変)
O(出力)は out(共変)
つまり、PECS 原則をそのまま型変換に適用しています。
まとめ
| キーワード | 方向 | 例 | 安全な操作 | 原則 |
|---|---|---|---|---|
out |
出すだけ | Producer<out T> |
読み取り専用 | Producer Extends |
in |
入れるだけ | Consumer<in T> |
書き込み専用 | Consumer Super |
覚え方の語呂:
「出すなら out、入れるなら in」
「Producer Extends, Consumer Super(PECS)」