ジェネリックメソッド

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

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

  • メソッド単独で型パラメータを取る ─ public static <T> T identity(T x) { return x; }
  • 戻り値の前に <T> を書く位置がポイント (static メソッドでも書ける)
  • 型推論で多くの場合 Solution.<String>identity("x")<String> は省略可
  • static メソッドにジェネリクスを付けるときは必須テクニック

ジェネリックメソッド とは

型パラメータをメソッド単位で宣言する ジェネリックメソッド を学び、配列やリストの先頭要素を返す汎用処理の書き方を身につけよう。

クラスではなくメソッドに型パラメータを付ける

ここまで ArrayList<Integer>ArrayList<String> のように、クラスに対して型パラメータを与える ジェネリッククラス を見てきました。Java のジェネリクスはそれだけではなく、メソッド 1 つに対しても型パラメータを付けることができます。これを ジェネリックメソッド と呼びます。

Java

public static <T> T firstOf(List<T> list) { if (list.isEmpty()) { return null; } return list.get(0); }

見慣れない <T>static と戻り値型 T の間にいきなり登場していますね。これは「このメソッドの中だけで通用する型パラメータ T をここで宣言します」という宣言で、Java の文法で必ずこの位置に書くと決まっています。Android アプリの SDK や Spring Boot の各種ユーティリティでも、内部ではこの記法がふんだんに使われています。

ジェネリッククラスの場合は class Box<T> { ... } のようにクラス名の右に <T> を書き、そのクラス全体で共通の型を表します。一方、ジェネリックメソッドは特定のメソッドだけのためのローカルな型パラメータで、呼び出すたびに別の型として推論されます。スコープが全然違うものだと意識してください。

なぜわざわざメソッド単位の型パラメータが要るのでしょうか。それは「クラス自体は型に依存していないけど、ある 1 つのメソッドだけは複数の型に対応したい」というケースがあるからです。たとえば Math クラスの max のような数値ユーティリティ、Collectionssortreverse、ファクトリメソッドの Optional.of などは、この ジェネリックメソッド の代表例です。

diagram (will load when visible)

図のとおり、同じ firstOf というメソッドでも、呼び出した引数の型に応じて TString になったり Integer になったりと、その場で変わります。コンパイラが文脈から T を埋めてくれる、というのがジェネリックメソッドの嬉しいところです。

文法 — 型パラメータの位置

ジェネリックメソッドの文法はかなり厳密で、<T> を書く位置を 1 文字でも間違えるとコンパイルが通りません。落ち着いて、左から右へ並びを確認してみましょう。

Java

public static <T> T firstOf(List<T> list) { ... } // ^^^^^^ ^^^ ^ ^^^^^^^ ^^^^^^^^^^^^ // 修飾子 宣言 戻り 名前 引数の型に T を使う // 位置 値型

要点は次の 3 つです。

  • 型パラメータの宣言 <T> は、戻り値型の直前 に置く
  • 修飾子 (public static final など) の後、戻り値型の前という順番は固定
  • <T> で宣言した T を、戻り値型・引数の型・メソッド本体の中で参照できる

もし public <T> static T firstOf(...) のように static<T> の順番を入れ替えるとコンパイルエラーになります。public static T <T> firstOf(...) のように戻り値の後に書くのもダメです。<T> は戻り値型の直前、と覚えてしまうのが早道です。

型パラメータの名前は慣習として、要素を表すなら T (Type)、キーなら K (Key)、値なら V (Value)、戻り値なら R (Result)、複数あるときは T, U のように 1 文字大文字を使います。<TypeOfElement> のように長い名前を付けても文法的には OK ですが、Java の世界では浮いてしまうので避けましょう。

型推論で <T> を省略する

ジェネリックメソッドを呼び出すとき、本当は Solution.<String>firstOf(list) のように山かっこで明示的に型を指定できます。ですが現代の Java では、ほとんどの場合この <String> を書きません。なぜならコンパイラが引数の型を見て勝手に推論してくれるからです。

Java

List<String> names = List.of("Alice", "Bob"); String head = Solution.firstOf(names); // T は String と推論される List<Integer> ids = List.of(1, 2, 3); Integer first = Solution.firstOf(ids); // T は Integer と推論される

namesList<String> 型なので、firstOf に渡した瞬間にコンパイラが「T = String だな」と判断してくれて、戻り値も String 扱いになります。これを 型推論 と呼びます。

型推論が効くおかげで、ArrayList<Integer> a = new ArrayList<>();<> (ダイヤモンド) も、var x = list.get(0); も自然に書けます。Java はかつて「型がうるさい」と言われていましたが、Java 7 以降の改善で KotlinScala に近い軽さで書けるようになってきました。

コード例 — Box クラスとの比較

ジェネリッククラスとジェネリックメソッドが混ざるとややこしく感じるので、対比して並べておきます。

Java

// ジェネリッククラス: クラス全体で共通の型 T public class Box<T> { private final T value; public Box(T value) { this.value = value; } public T get() { return value; } } // ジェネリックメソッド: そのメソッドだけの T public class Util { public static <T> T identity(T value) { return value; } }

Box<T> のほうは、new Box<String>("hi") のようにインスタンスを作る瞬間に T が決まり、その箱は最後まで String 専用です。一方 Util.identity のほうは呼ぶたびに T が決まり、同じファイルの中で Util.identity(10) (T = Integer) と Util.identity("x") (T = String) を共存させられます。

境界 (bounded type parameter) の予告

型パラメータには「どんな型でも OK」だけでなく、「Comparable を実装している型に限定したい」のような 制約 を付けることもできます。これを 境界 と呼び、次回以降の章で詳しく扱います。

Java

// T は Comparable<T> を実装している型だけ受け付ける public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; }

この書き方なら、max(3, 5)max("apple", "banana") も呼べますが、compareTo を持っていない自作クラスを渡すとコンパイル時に弾かれます。型でバグを未然に防ぐ仕組みになっています。今は「そういう発展形があるんだな」程度で OK です。

よくある間違い

ジェネリックメソッドを書くときに、初心者がほぼ必ずやらかすミスを 3 つ整理しておきます。

  • <T> を戻り値の後ろに書いてしまうpublic static T <T> firstOf(...) のように書くとコンパイルエラーです。必ず 戻り値型の直前<T> を置きましょう
  • raw 型のリストを渡してしまうList raw = new ArrayList(); のようなジェネリクスなしのリストを渡すと、TObject 扱いになり警告が出ます。raw 型は禁忌 と覚えて、必ず List<String> のように型を指定してください
  • 型推論が効かない場面で <> を省くvar x = Util.firstOf(List.of()); のように空のリストを直接渡すと推論できず Object になります。そういう時は Util.<String>firstOf(List.of()) のように明示的に型を指定する必要があります

もう 1 つよくあるのが、<T>String のような具体的なクラス名と混ぜて宣言してしまうパターンです。<T extends String> のように書くとそれは「境界付きの型パラメータ」になり、T という名前はまだ未確定の型を表します。<T> の T は これから決まる型のあだ名 であって、String を指す別名ではないことを意識してください。

エラーメッセージで cannot find symbol: class TT cannot be resolved to a type が出たら、9 割は <T> の宣言を忘れているか、書く位置を間違えています。落ち着いて戻り値型の直前を確認しましょう。

実務で遭遇するパターン

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

  • ユーティリティクラスCollections.emptyList() Optional.of(x) のような static <T> メソッド群
  • Builder パターンBuilder.<User>of(...) のように呼び出し側で型を指定する DSL
  • JSON マッパーmapper.readValue(json, User.class) の戻り値を呼び出し側の型に合わせる
  • 比較ユーティリティComparators.<T>thenComparing(...) のチェーン

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

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

  • 型パラメータの位置が間違いstatic T <T> identity(...) ではなく static <T> T identity(...)
  • ジェネリックメソッドにする必要がないのに <T> ─ 引数が決まり切っていて型パラメータが冗長な設計
  • <T extends Number> で済むのに <T> ─ 境界を絞れる場合は絞ったほうが安全
  • 呼び出し側で明示型を書きすぎ ─ Java の型推論で省ける場合は省くのが現代風

パフォーマンス考慮事項

  • ジェネリクスはコンパイル時のみ ─ 実行時オーバーヘッドはゼロ
  • 型消去で Object 化される ─ プリミティブ引数はオートボクシングが入る
  • 過剰な汎化は IDE が遅くなる ─ 引数が T だらけだと推論が深くなる
  • <T extends Comparable<T>> の再帰境界 ─ 強力だが読みづらく、新人には説明コスト高
この章のポイント

ここまでの要点 メソッド宣言の戻り値型の前に <T> を置けばメソッド単体で型パラメータが取れる。型推論で呼び出し側はほぼ省略可能。

やってみよう

今回の課題は、int 配列の先頭要素を返す firstOf メソッドを書くことです。本来であれば <T> T firstOf(List<T> list) のような汎用版を書きたいところですが、Java の executor との都合上、テストを通しやすい int[] バージョンで書いてもらいます。本文で学んだ「型パラメータでメソッドを汎用化する」イメージを頭の片隅に置きながら、まずは int の世界で「空ならどうするか」「あるならどう返すか」を実装してみましょう。

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

  1. Solution.firstOf(int[] arr) の中で、arr.length0 なら 0 を返す
  2. そうでなければ arr[0]return する
  3. テストは firstOf(new int[]{5, 10, 15})5、空配列で 0new int[]{42}42 のように振る舞うことを確認します

余力があれば、解いた後に「もしこれを <T> T firstOf(List<T> list) で書き直したらどうなるか」を頭の中で考えてみてください。intT に、arr.length == 0list.isEmpty() に、arr[0]list.get(0) に置き換えるだけで、ジェネリックメソッド版があっという間に完成します。型パラメータの威力をぜひ実感してみてください。

よくある質問

Q. ジェネリクスを使うと何が嬉しいですか?

A. 型安全と再利用性が両立します。Box のように型パラメータで宣言すれば Box、Box として使い回せ、コンパイル時に型不一致を検出できます。キャストや instanceof が減るため、実行時エラーも防げます。

Q. ワイルドカード ? extends と ? super の違いは?

A. ? extends T は「T のサブクラス」で読み取り専用、? super T は「T のスーパークラス」で書き込み専用というイメージです。PECS(Producer Extends, Consumer Super)の頭文字で覚えるのが定番です。データを取り出すなら extends、入れるなら super を選びます。

Q. ジェネリクスはコンパイル後も残りますか?

A. Java は型消去(erasure)方式のため、実行時には Object になります。そのため List と List は実行時に区別できず、instanceof List はできません。リフレクションで型情報を取りたい場合は TypeToken のようなトリックが必要です。

次のレッスン

次は 境界型パラメータ で、型パラメータをメソッド単位で宣言する ジェネリックメソッド を学び、配列やリストの先頭要素を返す汎用処理の書き方を身につけよう を学びます。

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

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

理解度チェック (30 秒)

Q. ジェネリックメソッド とは何か、1 文で説明してください。

この章のポイント

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

関連レッスン

要件

  1. メソッドは public static int firstOf(int[] arr) のシグネチャを維持すること
  2. arr.length == 0 のとき 0 を返すこと
  3. 空でないときは arr[0] をそのまま return すること (arr[1] などにしない)

入出力例

test-cases.txt

firstOf([5,10,15])5 firstOf([])0 firstOf([42])42 firstOf([-3,-1,-2])-3

ヒント

main.java
main.java
学習モード

メモ

ジェネリックメソッド

⌘S で保存