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

Java Java
スポンサーリンク

上限境界(extends)を一言でいうと

ジェネリクスの上限境界(extends)は、

この型パラメータには、あるクラス T か、そのサブクラスだけを許可します

と制限をかけるための仕組みです。

<T extends Number>
List<? extends Number>

のように書きます。

T は何でもいいよ」ではなく、
TNumber の一族だけにしたい」「Animal の一族だけにしたい」
といったときに使います。

ここでは、クラスの型パラメータに対する extends と、ワイルドカード ? extends の二つを、例を交えながら丁寧に見ていきます。


クラスの型パラメータに対する上限境界:<T extends 〜>

まずは素のジェネリクスと比較してみる

ジェネリクスなしの「Box」クラスはこうでした。

public class Box {
    private Object value;

    public void set(Object value) { this.value = value; }
    public Object get() { return value; }
}
Java

ジェネリクスありで「何でも入る Box」はこう。

public class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}
Java

Box<T> は、Box<String> にも Box<Integer> にも Box<User> にもできます。
「T に何を入れてもいい」という状態です。

ここで、「TNumber か、そのサブクラスにだけしたい」と思ったらどうするか。

<T extends Number> の意味

次のように書きます。

public class NumberBox<T extends Number> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }

    public double toDouble() {
        return value.doubleValue();  // Number のメソッドが使える
    }
}
Java

<T extends Number> と書いた瞬間に、

「T として使えるのは Number か、そのサブクラスだけ」
「クラスの中では、T を Number として扱っていい」

という二つのことが同時に保証されます。

使う側はこうなります。

NumberBox<Integer> intBox = new NumberBox<>();
intBox.set(10);
System.out.println(intBox.toDouble());  // 10.0

NumberBox<Double> doubleBox = new NumberBox<>();
doubleBox.set(3.14);
System.out.println(doubleBox.toDouble());  // 3.14

// NumberBox<String> x = new NumberBox<>();  // コンパイルエラー
Java

NumberBox<String> はコンパイルエラーになります。
StringNumber を継承していないからです。

ここが「上限境界」の本質です。

「T に許される“上限(最大の親)”は Number まで。それより上は不可」

という制限を付けているイメージです。

クラスの中で「上限のメソッド」が安全に使えるという強み

<T extends Number> のおかげで、クラス内部では

value は少なくとも Number
したがって doubleValue() などの Number のメソッドが必ず存在する

という前提で書けます。

もし <T> だけだと、value.doubleValue() と書けません。
T が何か分からないからです。

extends によって、「T は最低でも Number として振る舞える」という契約を結んでいるわけです。

同じパターンはよく出てきます。

<T extends Comparable<T>> として、compareTo が必ず呼べるようにする。
<T extends Animal> として、move()eat() が必ずある前提で書く。

こういう「上限境界で“能力”を縛る」という設計は、ジェネリクスでとてもよく使います。


メソッドの型パラメータに対する上限境界

<T extends Number> をメソッドに付ける

クラス全体ではなく、「このメソッドだけ Number 系にしたい」というケースもあります。
例えば、「任意の Number 配列の合計を取るメソッド」を考えましょう。

public class NumberUtils {

    public static <T extends Number> double sum(T[] array) {
        double total = 0;
        for (T n : array) {
            total += n.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        Integer[] ints = {1, 2, 3};
        Double[] doubles = {1.5, 2.5};

        System.out.println(sum(ints));     // 6.0
        System.out.println(sum(doubles));  // 4.0
        // String[] strings = {"a", "b"};
        // sum(strings);                    // コンパイルエラー
    }
}
Java

ここでは、メソッド宣言がこうなっています。

public static <T extends Number> double sum(T[] array)

<T extends Number> は、このメソッド専用の型パラメータ宣言です。

「ここで使う T は、Number の一族だけにします」
「だから、このメソッドの中では T を Number として扱ってOKです」

という意味になります。

実際、ループの中で n.doubleValue() を呼べています。

sum(strings) がコンパイルエラーになるのも、「StringNumber を継承していないから」です。

クラスの上限境界とメソッドの上限境界の違いは、「スコープ(どこまで有効か)」だけです。
宣言の形はほぼ同じ考え方で理解できます。


ワイルドカード ? extends T の上限境界との違い

<T extends Number> と ? extends Number は何が違うのか

よく混ざるのが、「名前付きの型パラメータ」と「ワイルドカード」です。

<T extends Number>
? extends Number

どちらも「Number のサブタイプだけ」という意味ですが、役割が少し違います。

<T extends Number>
何かの宣言側(クラスやメソッド)で「この T は Number 一族」と名前を付ける。
そのメソッド/クラス内で、ずっと同じ T を使い回す。

? extends Number
引数など「利用側」で、“名前はいらないが“Number 一族の何か”を受け取る”という場面で使う。

例えば、次の二つを比べます。

public static <T extends Number> double sum1(List<T> list) { ... }

public static double sum2(List<? extends Number> list) { ... }
Java

どちらも、「Number 系の List から合計を出したい」という用途に使えます。

初心者向けの感覚としては、

「メソッドの中で型に名前を付けて、T として扱い続けたいなら <T extends>
「ただ Number として読めればよくて、名前はいらないなら ? extends

くらいで捉えておくと分かりやすいです。

List<? extends Number> に対して「読み取りだけOK、書き込みはほぼ不可」という性質

上限境界ワイルドカード ? extends については、ワイルドカードの説明でも触れましたが、
もう一度「上限境界」の視点から整理します。

List<? extends Number> という引数を持つメソッドを考えます。

public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();
    }
    return total;
}
Java

このメソッドは、

List<Integer>
List<Double>
List<Long>

など、いろんな Number 系リストを受け取れます。

しかし、list.add(...) のように「具体的な値を追加」することはほぼできません。

なぜか。

List<? extends Number> の実体が、List<Integer> なのか List<Double> なのか List<BigDecimal> なのか分からないからです。

「この 1 行は全ての可能な実体に対して安全か?」をコンパイラが考えたとき、
どんな具体的な Number を add しても、ある型に対しては不正になる可能性があります。

だからコンパイラは「読み取り専用に近い扱い」に制限します。

ここでも、

extends で上限を決めた時点で、“その上限型として読む”ことは保証されるが、“具体的なサブタイプを書き込む”のは危険」

という性質が見えてきます。


上限境界を使う典型パターン

1. 比較可能なものだけ受け取る:<T extends Comparable<T>>

例えば、「リストの中から最大値を返したい」メソッドを考えます。

要素どうしを比べられないと困るので、「TComparable<T> を実装しているものだけ」にしたい。

public static <T extends Comparable<T>> T max(T a, T b) {
    return (a.compareTo(b) >= 0) ? a : b;
}
Java

<T extends Comparable<T>> という上限境界のおかげで、

T として渡される型は compareTo を必ず持っている
a.compareTo(b) が安全に呼べる

という前提で書けます。

使う側はこうです。

System.out.println(max(3, 5));           // OK(Integer は Comparable<Integer>)
System.out.println(max("a", "b"));       // OK(String は Comparable<String>)
// max(new Object(), new Object());      // コンパイルエラー
Java

ObjectComparable ではないので、最後の呼び出しはコンパイルできません。

「このメソッドは、“比較できる型”にしか使わせない」という制約を、
上限境界で表現しているわけです。

2. ある抽象クラス/インターフェースの一族にだけ絞る

例えば、「画面に描画できるもの」Drawable インターフェースがあるとします。

public interface Drawable {
    void draw();
}
Java

Drawable の一族をまとめて扱うコンテナ」を作るなら、素直に List<Drawable> でもいいのですが、
クラスにジェネリクスで縛りを入れておくこともできます。

public class Canvas<T extends Drawable> {
    private final List<T> elements = new ArrayList<>();

    public void add(T element) {
        elements.add(element);
    }

    public void drawAll() {
        for (T e : elements) {
            e.draw();
        }
    }
}
Java

これで Canvas<Circle>, Canvas<Rectangle> など、

Drawable を実装した型だけを受け付ける Canvas」

が作れます。

Canvas<String> のような「描画できないもの」はコンパイルエラーになります。

「このクラスは、Drawable の世界の中でだけ完結してほしい」
そんな意図を、 <T extends Drawable> という上限境界で表現しているわけです。


上限境界を使う時に意識しておきたいこと

「能力」を前提に設計したいときに使う

上限境界の本質は、「この型パラメータは、少なくともこの“能力”を持っている」という前提を作ることです。

<T extends Number>
→ 「数値として扱える(doubleValue などが呼べる)」

<T extends Comparable<T>>
→ 「大小比較できる(compareTo が呼べる)」

<T extends Runnable>
→ 「スレッドとして走らせられる(run が呼べる)」

こうしておくと、クラスやメソッドの中身を書くときに、
T に何でも書けるわけではない分、確実に存在するメソッドに頼った設計ができます。

逆に、上限境界を付けない T は、

「何も約束しない。Object としてしか扱えない」

という状態になります。

「上限を強くしすぎると汎用性を失う」バランス感覚

一方で、上限境界を付けすぎると、そのクラス/メソッドは「特定の世界でしか使えない」ものになります。

何でも受けたい汎用ユーティリティなら、あえて <T> のままにしておいた方がいいことも多いです。

<T extends Number> にするか、ただの <T> にするかは、

「この機能は Number 一族に限定した方が自然か?
それとも本当にどんな型でも使ってほしいのか?」

という設計の判断になります。

ここは答えが一つではなく、「どう使ってほしいか」「どう誤用してほしくないか」を考えて決める部分です。


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

ジェネリクスの上限境界(extends)を一言でまとめると、

「型パラメータに“このクラス/インターフェース一族だけ”という上限を付けて、その“能力”を前提にコードを書けるようにする仕組み」

です。

<T extends Number>
<T extends Comparable<T>>
List<? extends Number>

といった形で、

「T は Number の一族」
「T は Comparable を実装している」
「このリストは Number 一族のどれか」

という制約を付けることで、

内部では Number/Comparable として安全に扱える
誤った型に対する利用をコンパイル時に防げる

というメリットが生まれます。

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