Java | Java 詳細・モダン文法:ジェネリクス – 下限境界(super)

Java Java
スポンサーリンク

下限境界(super)を一言でいうと

ジェネリクスの下限境界 ? super T は、

「このコレクションは T か、その“親クラスたち”のどれかを要素型として持つ。だから、T を入れるのは安全だよ」

と宣言するための仕組みです。

List<? super Integer>
と書いた瞬間、

List<Integer>List<Number>List<Object> か、そのへんのどれかだけ受け取ります」
「だから、このリストに Integeradd するのは安全です」

という意味になります。

ここでのキーワードは、PECS 原則の「Consumer Super」
つまり「要素を“消費する(受け取る)”側には ? super を使う」という考え方です。


まずは「? なし」との違いから入る

List<Integer> にだけ add したいメソッド

例として、「リストに整数を 3 つ追加するメソッド」を考えます。

単純に書くとこうなります。

import java.util.List;

public class Example1 {
    public static void addThreeIntegers(List<Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
}
Java

これは List<Integer> に対しては問題なく動きます。

List<Integer> ints = new ArrayList<>();
addThreeIntegers(ints);
Java

でも、こんなことはできません。

List<Number> numbers = new ArrayList<>();
// addThreeIntegers(numbers);   // コンパイルエラー
Java

「Number のリストに Integer を足して何が悪いの?」と思うかもしれませんが、
ジェネリクスは不変(List<Integer>List<Number> のサブタイプではない)なので、
これはそもそも別物として扱われてしまいます。

ここで下限境界 ? super Integer が登場します。


List<? super Integer> のイメージ

「Integer を受け入れられる親クラスたちのリスト」を表す

こう書き換えてみます。

import java.util.List;

public class Example2 {
    public static void addThreeIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
}
Java

List<? super Integer> は、

「要素型が Integer か、その親クラス(Number や Object)のリストなら何でも受け取る」

という意味になります。

呼び出し側はこう書けます。

List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

Example2.addThreeIntegers(ints);
Example2.addThreeIntegers(numbers);
Example2.addThreeIntegers(objects);
Java

どの呼び出しもコンパイル OK です。

なぜかというと、

List<Integer> に Integer を add → もちろん安全
List<Number> に Integer を add → Integer は Number のサブクラスなので安全
List<Object> に Integer を add → Integer は Object のサブクラスなので安全

になっているからです。

言い換えると、

? super Integer とは “Integer を受け入れられるだけの器” を意味する」

と考えるとイメージしやすいと思います。


下限境界で「できること」と「できないこと」

add はできるが、get はほぼ Object

List<? super Integer> に対して、どんな操作が安全かを見てみます。

public static void demo(List<? super Integer> list) {
    list.add(10);        // OK
    list.add(20);        // OK

    Object o = list.get(0);     // これは OK
    // Integer i = list.get(0); // これはコンパイルエラー
}
Java

ここが「super むずい」と感じやすいポイントです。

add(10) が OK なのはさっき説明した通り、「どの可能な型(Integer, Number, Object)に対しても安全だから」です。

一方、get で取り出すときには状況が変わります。

List<? super Integer> の実体は、次のどれかです。

List<Integer>
List<Number>
List<Object>

では、list.get(0) で戻ってくるものを、「必ず Integer だ」と言い切れるでしょうか?

List<Integer> ならまだしも、
List<Number>List<Object> であれば、もともと DoubleString が入っているかもしれません。

コンパイラは「ここに入っているのが必ず Integer」とは判断できません。

だから安全なのは「Object として受け取ることだけ」です。

Object o = list.get(0);  // これなら、どの実体でもコンパイル的に安全
Java

ここから分かる重要な性質は、

? super T のリストは、「T を突っ込む(消費する)には向いているが、取り出したものは Object としてしか扱えない

ということです。


PECS の「Consumer Super」をしっかり腹に落とす

PECS 原則の中の “C = Consumer Super”

PECS という有名な覚え方があります。

Producer Extends, Consumer Super(PECS)

Producer(生産者)には ? extends
Consumer(消費者)には ? super

リストの立場から見て、

そのリストから値を「取り出す」だけなら、それは Producer(生産者)
そのリストに値を「詰め込む」だけなら、それは Consumer(消費者)

と考えます。

下限境界 ? super T は、「T を消費するコレクション」を表現するのに向いています。

先ほどの addThreeIntegers の例は、まさに Consumer です。

public static void addThreeIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}
Java

このメソッドは「Integer を list に詰め込むだけ」で、中身を取り出していません。
つまり、「list は Integer を消費する側」= Consumer です。

だから ? super Integer がぴったりハマる、というわけです。


上限境界(extends)との対比で理解を深める

extends は「読み取りに強い」、super は「書き込みに強い」

? extends T? super T を並べてみます。

List<? extends Number>
List<? super Integer>

? extends Number は、「Number を“生産(取り出す)”側」に向いています。

public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {    // Number として読み取れる
        total += n.doubleValue();
    }
    return total;
}
Java

ここでは list.add(...) はほぼできないけれど、
Number として読み出すのは自由です。

一方 ? super Integer は、「Integer を“消費(受け取る)”側」に向いています。

public static void addInts(List<? super Integer> list) {
    list.add(10);  // OK
    list.add(20);  // OK
}
Java

ここでは list.add(Integer) は安全にできるけれど、
取り出したものは Object としてしか扱えません。

この性質を一言で言うと、

? extends T
「Read-only(読み取り専用)寄り。T として get できるが、add はダメ」

? super T
「Write-only(書き込み専用)寄り。T を add できるが、get は Object」

という感じです。

「型階層」を図でイメージする

整数まわりのクラス階層をざっくり描くと、こうなります。

Object
└ Number
  └ Integer

このとき、

? extends Number
→ Number か、その下(Integer など)。つまり「上から見て“子側”に広がる」イメージ。

? super Integer
→ Integer か、その上(Number, Object)。つまり「下から見て“親側”に広がる」イメージ。

super はまさに「親」という意味なので、
「このワイルドカードの下限(最低ライン)は Integer です。そこから上の親クラスなら OK」
という解釈になります。


実用的な例:汎用的な add メソッド

「いろんなリストに整数をまとめて放り込みたい」ユーティリティ

もう少し実務っぽいイメージを出してみます。

たとえば、「いろんな種類のリストに整数を詰め込みたい」という状況。

public static void fillWithIntegers(List<? super Integer> list, int n) {
    for (int i = 0; i < n; i++) {
        list.add(i);
    }
}
Java

呼び出し側:

List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();

fillWithIntegers(ints, 3);
fillWithIntegers(nums, 3);
fillWithIntegers(objs, 3);
Java

それぞれ、

ints → 要素は Integer
nums → 要素は Number だが、中身として Integer が入っている
objs → 要素は Object だが、中身として Integer が入っている

という状態になります。

ここで大事なのは、

「fillWithIntegers は、“どの具体的なリスト型でもいいから、とにかく Integer を安全に詰め込みたい”ユーティリティ」

だということです。

その意図を型で表現したのが List<? super Integer> です。


ジェネリックメソッドと ? super の組み合わせ

型パラメータ T と下限境界を合わせて使う

さらに一歩進めて、型パラメータと下限境界を組み合わせるパターンもあります。

例えば、「あるリストから別のリストに要素をコピーする」メソッド。

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

ここでは、

src? extends T(T の生産者)
dest? super T(T の消費者)

という設計になっています。

呼び出し側はこう書けます。

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();

CopyUtil.copy(ints, nums);  // OK
CopyUtil.copy(ints, objs);  // これも OK
// CopyUtil.copy(nums, ints); // コンパイルエラー
Java

ここでも PECS がそのまま出てきています。

src(Producer) → extends
dest(Consumer) → super

このように、「T のデータがどちらの方向に流れているか」を意識すると、
extendssuper の選び分けがだんだん自然になってきます。


どんなときに下限境界を使うべきか

ざっくりした判断基準

自分のメソッドが「どんな立場でコレクションを扱っているのか」を考えてみてください。

そのメソッドが、「コレクションから要素を読み取るだけ」なら Producer(extends)
「コレクションに要素を詰め込むだけ」なら Consumer(super)

です。

特に ? super T を使うべきなのは、

「T を add したいが、引数には List<T> だけじゃなくて List<親クラス> も受け取りたい」

というときです。

たとえば、

「多態性(ポリモーフィズム)を活かして、List<Animal>Dog を入れたい」
List<Object> を“何でも倉庫”として扱い、StringInteger を詰め込みたい」

といった場面で役立ちます。

逆に、get も add も両方したくて、かつ柔軟さも欲しい、となると
ワイルドカードではなく <T> 型パラメータを使った方がシンプルに書けることが多いです。


まとめ:下限境界(super)を自分の中でこう位置づける

下限境界 ? super T を一言で整理すると、

T を“安全に add できる器”を表すためのワイルドカード。T の親クラスたちをまとめて扱うための型指定

です。

具体的には、

List<? super Integer>
List<Integer>, List<Number>, List<Object> などを受け取れる
Integer を add するのはどれに対しても安全
→ 取り出した値は実質 Object としてしか扱えない

そして、PECS 原則の「Consumer Super」
「値を詰め込む(consume)だけなら ? super を使う」と覚えておくと、
下限境界を使う場面がかなりクリアになります。

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