Java | Java 詳細・モダン文法:ジェネリクス – PECS 原則

Java Java
スポンサーリンク

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(...) は一切していません。
つまり、このメソッドにとって listProducer(生産者)です。

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 して何かすることはありません。

つまり、このメソッドにとって listConsumer(消費者)です。

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”なのか」という関係も表現できる。

タイトルとURLをコピーしました