下限境界(super)を一言でいうと
ジェネリクスの下限境界 ? super T は、
「このコレクションは T か、その“親クラスたち”のどれかを要素型として持つ。だから、T を入れるのは安全だよ」
と宣言するための仕組みです。
List<? super Integer>
と書いた瞬間、
「List<Integer> か List<Number> か List<Object> か、そのへんのどれかだけ受け取ります」
「だから、このリストに Integer を add するのは安全です」
という意味になります。
ここでのキーワードは、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);
}
}
JavaList<? 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> であれば、もともと Double や String が入っているかもしれません。
コンパイラは「ここに入っているのが必ず 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 → 要素は Integernums → 要素は 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 のデータがどちらの方向に流れているか」を意識すると、extends と super の選び分けがだんだん自然になってきます。
どんなときに下限境界を使うべきか
ざっくりした判断基準
自分のメソッドが「どんな立場でコレクションを扱っているのか」を考えてみてください。
そのメソッドが、「コレクションから要素を読み取るだけ」なら Producer(extends)
「コレクションに要素を詰め込むだけ」なら Consumer(super)
です。
特に ? super T を使うべきなのは、
「T を add したいが、引数には List<T> だけじゃなくて List<親クラス> も受け取りたい」
というときです。
たとえば、
「多態性(ポリモーフィズム)を活かして、List<Animal> に Dog を入れたい」
「List<Object> を“何でも倉庫”として扱い、String や Integer を詰め込みたい」
といった場面で役立ちます。
逆に、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 を使う」と覚えておくと、
下限境界を使う場面がかなりクリアになります。
