PECS 原則を一言でいうと
PECS 原則は、ジェネリクスのワイルドカードを使うときの合言葉です。
Producer Extends, Consumer Super
「値を“生産する(取り出す)側”には ? extends を使い、
値を“消費する(受け取る)側”には ? super を使いなさい」
というルールです。
これをちゃんと腹に落としておくと、
「ここ ? extends? ? super? どっちだっけ?」という迷いがかなり減ります。
まず「生産者」と「消費者」のイメージを固める
コレクションは「中身を出す側」か「中身を受け取る側」か
PECS でいう Producer(生産者)と Consumer(消費者)は、
「そのメソッドがコレクションに対して何をしているか」で決まります。
コレクションから要素を取り出して使うだけなら、そのコレクションは Producer。
コレクションに要素を追加するだけなら、そのコレクションは Consumer。
例えば、リストの中身を全部表示するメソッドはどうでしょう。
public static void printAll(List<?> list) {
for (Object e : list) {
System.out.println(e);
}
}
Javaこの list は、要素を「取り出すだけ」です。
要素を add していないので、このメソッドにとっての list は Producer です。
逆に、「リストに 1, 2, 3 を詰め込むだけ」のメソッドならどうか。
public static void addThreeIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
Javaこの list は、要素を「受け取るだけ」です。
中身を読んで使ったりしていないので、Consumer です。
PECS は、「このメソッドから見て、そのコレクションは Producer か Consumer か?」を意識しなさい、という考え方です。
Producer には extends:? extends T の役割
「T を生産する(取り出す)側」の典型例:数値の合計を取る
次のメソッドを見てください。
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
Javaこのメソッドは、list から Number を取り出して、合計を計算しています。list 自体は要素を「生産してくれる側」です。
List<? extends Number> と書くことで、
List<Integer>List<Double>List<Long>
など、ありとあらゆる「Number のサブタイプのリスト」を受け取れます。
ここで重要なのは、メソッドの中で list に対してしていることは
「Number として取り出している」だけだという点です。
list.add(...) は一切していません。
つまり、このメソッドにとって list は Producer(生産者)です。
Producer に対しては extends を使う。
これが PECS の “P E” の部分です。
? extends の性質を言い換える
List<? extends Number> のように書いた時の性質を、PECS 目線で整理するとこうなります。
このメソッドから見て、list は「Number を生産してくれる箱」。
だから「Number として読み出す」ことができる。
しかし、中身が List<Integer> か List<Double> か分からないので、値を安全に add することはできない。
つまり、? extends T は「T を get するには向いているが、add するには向いていない」型です。
Producer(生産者)向けのワイルドカードだと捉えれば、記憶に定着しやすくなります。
Consumer には super:? super T の役割
「T を消費する(受け取る)側」の典型例:整数を詰め込む
次のメソッドを見てください。
public static void addThreeIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
Javaここで list は、Integer を add されまくる箱です。
中身を get して何かすることはありません。
つまり、このメソッドにとって list は Consumer(消費者)です。
List<? super Integer> と書くことで、
このメソッドは、
List<Integer>List<Number>List<Object>
など「Integer を安全に add できるあらゆる箱」を受け取れるようになります。
なぜ安全なのか。
Integer は Number のサブクラスであり、Object のサブクラスでもあるので、
List<Integer> に Integer を入れる → OK
List<Number> に Integer を入れる → OK
List<Object> に Integer を入れる → OK
になるからです。
? super T は、「T を突っ込んでも絶対に破綻しない“親側の型”」を許可する宣言です。
Consumer に対しては super を使う。
これが PECS の “C S” の部分です。
? super の性質を言い換える
List<? super Integer> の性質を、PECS の観点で整理するとこうなります。
このメソッドから見て、list は「Integer を飲み込んでくれる箱」。
だから「Integer を add する」のは安全。
しかし、中に実際に何が入っているかは分からないので、取り出したものは Object としてしか扱えない。
つまり、? super T は「T を add するには向いているが、get した値の型には期待できない」型です。
Consumer(消費者)向けのワイルドカードだと覚えると、挙動の意味が見えてきます。
PECS をコードで感じる:コピー関数の例
よくある定番パターン:copy(src, dest)
「あるコレクションから別のコレクションに要素をコピーする」メソッドを、
ジェネリクスでちゃんと書くとこうなります。
import java.util.List;
public class CopyUtil {
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
}
}
Javaここが PECS の教科書的な使い方です。
src は、T を「生産する側」なので ? extends T。dest は、T を「消費する側」なので ? super T。
まさに PECS(Producer Extends, Consumer Super)がそのままコードに表れています。
使い方を見てみます。
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new java.util.ArrayList<>();
List<Object> objs = new java.util.ArrayList<>();
CopyUtil.copy(ints, nums); // OK: Integer を Number にコピー
CopyUtil.copy(ints, objs); // OK: Integer を Object にコピー
// CopyUtil.copy(nums, ints); // コンパイルエラー
Javaこのメソッドが許しているのは、
「ある型 T の要素を、“T の親クラスたちの箱”へコピーする」
という関係です。
T を生産する側 → ? extends T
T を消費する側 → ? super T
という関係を意識すると、「なぜこの型引数の並びになるのか」が腑に落ちるようになります。
? を使うか <T> を使うかの境目も PECS で決められる
「関係性がある」なら <T>、「その場の役割だけ」なら ?
ワイルドカードは ? です。
型パラメータは <T> です。
どちらもジェネリクスですが、使う場面が少し違います。
あるメソッドの中で、「引数 A と引数 B は同じ型 T で、戻り値も T」というように、
型どうしの“関係”を表現したい場合は <T> を使います。
例えば、コピー関数は <T> がメインで、? は「Producer / Consumer の役割」にだけ使われています。
public static <T> void copy(List<? extends T> src, List<? super T> dest)
Java一方、「この引数は Producer としてだけ使う」「この引数は Consumer としてだけ使う」
という“その場の役割”だけ表現できればよくて、他と型の関係を持たせる必要がないなら、List<? extends Number> や List<? super Integer> のように、ワイルドカードだけで完結させられます。
PECS を意識すると、
「ここは Producer だから ? extends で十分」
「ここは Consumer だから ? super で十分」
「ここは引数と戻り値の関係まで表したいから <T> も使う」
という判断がしやすくなります。
上限境界(extends)と下限境界(super)の直感的な違いをもう一度整理する
グラフの向きのイメージ
型階層を簡単な例で思い浮かべてください。
Object
└ Number
└ Integer
このとき、T = Integer として考えます。
? extends Integer
→ Integer か、その子(ここでは Integer 自身だけ)。
→ 上から見て「下方向」にしか広がらないイメージ。
? super Integer
→ Integer か、その親(Number や Object)。
→ 下から見て「上方向」に広がるイメージ。
Producer Extends(? extends T)
→ 取り出すときは T までしか期待しないけど、その下位型も受け付ける。
Consumer Super(? super T)
→ T を入れるときに、T を受け入れられるだけの親型たちを受け付ける。
PECS は、この「どっち向きに広がるか」を自動的に思い出させてくれます。
実務の中での目安:PECS が活きる瞬間
「引数の List に対して、何をしているか」を見る癖をつける
実際にコードを書いていて、「ここ、List<T> じゃなくて List<? extends T> にすべき?」
「List<? super T> の方がいい?」と思ったら、まずこれを自問してください。
このメソッドは、その List から「値を取り出すだけ」か?
それとも、その List に「値を追加するだけ」か?
両方やっているのか?
取り出すだけなら、その List は Producer。
→ ? extends を検討する。
追加するだけなら、その List は Consumer。
→ ? super を検討する。
両方やっているなら、ワイルドカードではなく <T> の方が素直なことが多いです。
まとめ:PECS 原則を自分の言葉で言い直す
最後に、PECS をあなた自身の言葉として再定義してみます。
Producer Extends, Consumer Super(PECS)
「値を読み出すだけのコレクション(Producer)には ? extends を。
値を書き込むだけのコレクション(Consumer)には ? super を。」
? extends T
→ T として get できるが、add はほぼできない。読み取り専用寄り。
? super T
→ T を add できるが、get したものは Object としてしか扱えない。書き込み専用寄り。
そして、<T> を混ぜることで、
「どの引数とどの戻り値が“同じ T”なのか」という関係も表現できる。
