境界型パラメータ

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • <T extends Number> で型パラメータに 上限境界 を付ける
  • 境界の中では T のメソッド (NumberdoubleValue 等) が呼べる
  • 複数境界 <T extends Number & Comparable<T>> のように & でつなげる (クラスは 1 つだけ)
  • 下限境界 (super) はジェネリックメソッドのワイルドカードでのみ使える

境界型パラメータ とは

ジェネリクスの型パラメータに extends で上限を付ける境界型パラメータを学び、Number を継承した型だけを受け取って合計を計算するメソッドを書いてみよう。

「何でも入る」では困る場面

ジェネリクスの基本では <T> と書けば、String でも Integer でも LocalDate でも、どんな型でも受け取れるメソッドが書けることを学びました。確かに便利なのですが、実務で書いていると「いや、ここはさすがに数値しか受け付けたくない」という場面が出てきます。たとえば「リストの合計を返すメソッド」を作りたいとき、String のリストが渡ってきても困りますし、そもそも .doubleValue() のような数値変換メソッドを呼べないと合計の計算が始まりません。

Java

// これだとコンパイルが通らない public static <T> double sumGeneric(List<T> list) { double total = 0; for (T value : list) { total += value.doubleValue(); // T には doubleValue が無い } return total; }

コンパイラから見ると T は「何になるか分からない型」なので、Object 由来のメソッド (toStringequals など) しか呼べません。doubleValue()java.lang.Number で定義されているメソッドなので、TNumber のサブクラスだと保証されていない以上、呼び出しは弾かれます。これを解決するのが、今回の主役 境界型パラメータ (bounded type parameter) です。

「型パラメータに最低限のラインを設ける」と覚えると分かりやすいです。T をまっさらにせず、「少なくともこの型は継承していてね」と縛ることで、その型のメソッドを安心して呼べるようになります。

文法 ─ <T extends 上限型>

境界型パラメータの書き方はとてもシンプルで、<T extends 上限となる型> のように extends で繋ぐだけです。<T extends Number> と書けば、「TNumber か、Number のサブクラス (Integer Double Long Float Short Byte など) でなければならない」という意味になります。

Java

import java.util.List; public class Calc { public static <T extends Number> double sum(List<T> list) { double total = 0; for (T value : list) { total += value.doubleValue(); } return total; } }

さっき弾かれていた value.doubleValue() が、今度はすんなり通ります。コンパイラは「TNumber のサブクラスなんだから、doubleValue() は当然持っているよね」と判断してくれるからです。呼び出し側は List<Integer> でも List<Double> でも List<Long> でも sum に渡せます。一方で List<String> を渡そうとすると、その時点でコンパイルエラーになり、実行する前にミスが分かります。

extends はクラスの継承で使う class A extends B と同じキーワードですが、ジェネリクスの世界では インターフェイスの実装extends と書きます。<T extends Comparable<T>> のように、implements は使わないことに注意してください。これは Java の文法の例外的な部分なので、暗記しておくと混乱しません。

diagram (will load when visible)

境界に指定できる型は、クラスでもインターフェイスでも構いません。クラスを境界にすると「そのクラス由来のメソッド」が、インターフェイスを境界にすると「そのインターフェイスのメソッド」が、T の値に対して呼べるようになります。

Comparable を境界にした並べ替え

境界の使いどころとしてもう 1 つよくあるのが、<T extends Comparable<T>> という形です。Comparable を実装した型だけを受け取れば、a.compareTo(b) で大小比較ができるので、「最大値を返すメソッド」や「ソート関連の処理」が書けるようになります。

Java

import java.util.List; public class Util { public static <T extends Comparable<T>> T max(List<T> list) { T best = list.get(0); for (T value : list) { if (value.compareTo(best) > 0) { best = value; } } return best; } }

Integer Double String LocalDate などはすべて Comparable を実装しているので、このメソッドにそのまま渡せます。Comparable を実装していない独自クラスを渡そうとすると、やはりコンパイル時に弾かれます。「実行してみないと分からない」ではなく「コンパイラが先に教えてくれる」のがジェネリクスの大事な価値です。

T 同士を比較する」ことを言いたいので、<T extends Comparable<T>> のように Comparable の山かっこにも T を入れます。慣れないと記号の海に見えますが、T 同士を比べたいというメッセージだと読めば腑に落ちます。

複数境界 ─ <T extends A & B>

さらに踏み込むと、境界は 1 つに限定されず、& で繋いで複数の制約を同時に課すこともできます。これを 複数境界 (multiple bounds) と呼びます。

Java

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

上の例では「TNumber のサブクラスで、かつ Comparable<T> を実装している型」という二段構えの縛りです。IntegerDouble も両方の条件を満たすので渡せますが、Number 側だけ満たして Comparable を実装していない独自クラスを定義した場合は弾かれます。

書き方のルールとして次の点に気をつけてください。

  • & で繋ぐ
  • クラスを入れる場合は 必ず最初 に書く (例: <T extends Number & Comparable<T>>)
  • 並べられるクラスは 1 個まで (Java は単一継承なので、<T extends ClassA & ClassB> は不可)
  • インターフェイスは何個でも & で繋いで OK

上限境界と下限境界の予告 ─ ワイルドカードの世界

今回学んでいる <T extends Number> のように、型パラメータの段階で extends を書く方法を 上限境界 (upper bound) と呼びます。これとは別に、メソッドの引数型として書く ワイルドカード (?) の世界では 下限境界 (lower bound) として <? super Integer> のような書き方も登場します。

Java

// 上限境界 (今回のテーマ) public static double sumUpper(List<? extends Number> list) { ... } // 下限境界 (次回以降のテーマ) public static void addInts(List<? super Integer> list) { ... }

上限と下限を使い分ける考え方は PECS (Producer Extends, Consumer Super) というキーワードで知られています。本レッスンでは細かな違いには立ち入りませんが、「型パラメータ自体に縛りをかけるのが上限境界、メソッド引数のワイルドカードで上下を表現するのが PECS」という大枠だけ頭に入れておくと、後の章がスムーズに読めます。

diagram (will load when visible)

よくある間違い

境界型はシンプルな見た目の割に、書き始めるとつまづきやすいポイントが多いです。代表的な落とし穴を 3 つ紹介します。

  • extendsimplements と書いてしまう — 普段クラスで implements Comparable と書いている習慣で、つい <T implements Comparable<T>> と書きたくなりますが、これは文法エラーです。ジェネリクスでは、相手がクラスでもインターフェイスでも、境界はすべて extends で揃えます
  • 境界に int などのプリミティブを書こうとする<T extends int> のような書き方はできません。ジェネリクスはそもそも 参照型 (クラス・インターフェイス) しか扱えないので、整数なら Integer、小数なら Double のように ラッパー型 を境界に指定します
  • 境界を付けないまま .doubleValue() 等を呼ぶ<T> だけだと、コンパイラは TObject 相当としか見られないので、Number のメソッドは呼べません。「数値専用の処理を書きたい」と思ったら、まず <T extends Number> を疑うクセを付けると安全です

もう一点、複数境界の <T extends A & B>, (カンマ) と & を混同するパターンも要注意です。型パラメータを複数並べる <T, U> ではカンマを使いますが、1 つの型パラメータに複数の境界を付けるときは必ず & を使います。エラーメッセージが分かりにくいので、書く順序と区切り記号は丁寧に確認しましょう。

業務コードでよく見る誤りとして、<T extends Number> と書きながらメソッド本体で int リテラルとの足し算 total += value をしてしまうケースがあります。Tint は直接足し算できないので、必ず value.doubleValue()value.intValue() を経由するのが基本です。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • 数値計算の共通化<T extends Number> double sum(List<T> list)Integer Long Double をまとめて処理
  • 比較可能な要素のソート<T extends Comparable<T>> T max(List<T> list)
  • Repository<T extends BaseEntity> ─ 業務エンティティに共通プロパティを extends で強制
  • Builder の連鎖<T extends MyBuilder<T>> でメソッドチェーンの戻り型を保つ

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • 境界が広すぎる<T> で十分なのに <T extends Object> を書いている
  • & Comparable の順序 ─ クラス境界は先頭、interface はあとに並べる
  • <T extends List<String>> ─ ここは具象 List<String> を要求しているが、本来 <T extends List<? extends String>> のほうが柔軟
  • <T extends Comparable<? super T>> ─ 慣用句、書き方を間違えるとレビューで指摘される

パフォーマンス考慮事項

  • 型消去で消える ─ 境界は実行時には残らない (実体は Number 等の親型)
  • ブリッジメソッドの生成 ─ オーバーライド時に追加メソッドが生成され、メモリが少し増えるが無視できるレベル
  • <T extends Comparable<T>> の制約緩和 ─ より柔軟な <T extends Comparable<? super T>> のほうが実用的
  • JIT インライン化 ─ 境界付きの呼び出しも最適化されるため実行時オーバーヘッドなし
この章のポイント

ここまでの要点 <T extends Bound> で上限境界、複数は & で連結。境界内では Bound のメソッドが呼べる。下限は <? super T> で表現。

やってみよう

それでは今回の課題に取り組んでみましょう。完成形の本文では <T extends Number> double sumNumbers(List<T> list) のような形を紹介しましたが、テストでは入出力を単純化するため、引数を int[] 配列にした版を実装してもらいます。境界型ジェネリクスの「気持ち」を理解した上で、Java 標準の int 配列で同じ「合計を返す」ロジックを書く、という流れです。

やることは次のとおりです。

  1. Solution.sumNumbers(int[] arr) を実装する
  2. 受け取った配列の各要素を足し合わせて int で返す
  3. 空配列 {} が渡されたら 0 を返す

本文のジェネリック版コードもぜひ自分の手で書き写してみて、「<T extends Number> を外したらどんなエラーが出るか」を確認してみてください。コンパイラに怒られながら、境界の役割が腑に落ちるはずです。書けたら次は List<Integer>List<Double> の両方を渡せるかを試して、境界型の威力を実感していきましょう。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は ワイルドカード ? で、ワイルドカード ? を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. 境界型 の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 境界型 とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Solution.sumNumbers(int[] arr)public static int で実装すること
  2. 配列の全要素を足し合わせた結果を int で返すこと
  3. 空配列が渡されたら 0 を返すこと (例外を投げない)

入出力例

test-cases.txt

sumNumbers([1,2,3])6 sumNumbers([])0 sumNumbers([-5,10,-3,8])10 sumNumbers([42])42

ヒント

main.java
main.java
学習モード

メモ

境界型パラメータ

⌘S で保存