ジェネリクスメソッドを一言でいうと
ジェネリクスメソッドは、
「メソッドの“中だけ”で使える型パラメータを宣言して、そのメソッドを型安全に汎用化する仕組み」
です。
public static <T> T first(T a, T b) のように、
メソッドの戻り値の“前”に <T> と書くやつが、まさにジェネリクスメソッドの宣言です。
クラス全体をジェネリクス化しなくても、
「このメソッドだけ型に柔軟性を持たせたい」「引数と戻り値の型の関係を型で表現したい」
というときに、ピンポイントで使えます。
まずは形を覚える:ジェネリクスメソッドの宣言位置
「戻り値の手前の <T>」がメソッドの型パラメータ宣言
最もシンプルなジェネリクスメソッドを書いてみます。
public static <T> T identity(T value) {
return value;
}
Javaこのとき、
public static のあとにある <T> が「メソッドの型パラメータ宣言」T identity(T value) の T たちは、その <T> を使った「型パラメータの利用」
です。
クラス宣言との違いは“場所”だけです。
クラスの場合は:
public class Box<T> {
// ここで T を使う
}
Javaメソッドの場合は:
public static <T> T method(T arg) {
// ここで T を使う
}
Javaこの「戻り値の手前の <T>」を見る習慣を持つと、
「あ、これはジェネリクスメソッドなんだな」と一発で分かるようになります。
呼び出し側はどう見えるか
さっきの identity を呼び出すコードは、こんな感じです。
String s = identity("hello"); // T は String と推論される
Integer i = identity(123); // T は Integer と推論される
Javaコンパイラは、渡された引数の型から「この呼び出しにおける T の型」を推論してくれます。
明示的に書くこともできますが、通常は省略します。
String s = GenericsExample.<String>identity("hello"); // こうも書けるがほぼ使わない
Javaクラスジェネリクス vs メソッドジェネリクス
クラスに対するジェネリクスだけでは足りない場面
「ジェネリクス=クラスに <T> を付けるもの」というイメージがあるかもしれませんが、
メソッド単位でジェネリクスにした方がキレイな場面はたくさんあります。
例えば、「配列をそのまま List に変換するユーティリティ」を考えます。
クラス全体をジェネリクスにする必要はなく、
この一つのメソッドだけ汎用的であればいいですよね。
import java.util.ArrayList;
import java.util.List;
public class CollectionsUtil {
public static <T> List<T> toList(T[] array) {
List<T> list = new ArrayList<>();
for (T e : array) {
list.add(e);
}
return list;
}
}
Javaこれは「クラスは普通のクラス」「メソッドだけがジェネリック」です。
使う側はこうです。
String[] names = {"Alice", "Bob"};
Integer[] nums = {1, 2, 3};
List<String> nameList = CollectionsUtil.toList(names); // T は String
List<Integer> numList = CollectionsUtil.toList(nums); // T は Integer
Javaクラスジェネリクスと違い、
「型パラメータのスコープがメソッド内部に閉じている」
これがジェネリクスメソッドの大きな特徴です。
クラスジェネリクスとメソッドジェネリクスを両方使うこともある
クラスそのものがジェネリクスで、
その中のメソッドが「さらに」別の型パラメータを持つこともあります。
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T get() { return value; }
public <U> U map(java.util.function.Function<T, U> mapper) {
return mapper.apply(value);
}
}
Javaここでは、
class Box<T> の <T> が「クラスの型パラメータ」public <U> U map(...) の <U> が「メソッドの型パラメータ」
として共存しています。
T は Box の「中に入っている型」
U は map メソッドが「変換したい先の型」
という役割です。
「クラスにぶら下がる型」と「メソッド一回ごとの型」は別物として扱える、ということを押さえておいてください。
典型パターン1:コレクションから最初の要素を返す
ジェネリクスメソッドのメリットを感じやすい小さな例
「リストの最初の要素を返す」メソッドを考えます。
ジェネリクス無しだとこうなります。
public static Object first(List list) {
return list.get(0);
}
Java使う側は毎回キャストが必要です。
List<String> names = List.of("Alice", "Bob");
String firstName = (String) first(names); // キャストが必要
Javaここをジェネリクスメソッドにすると、こう書けます。
public static <T> T first(List<T> list) {
return list.get(0);
}
Java使う側ではキャスト不要になります。
List<String> names = List.of("Alice", "Bob");
List<Integer> nums = List.of(1, 2, 3);
String firstName = first(names); // 戻り値の型は String
Integer firstNum = first(nums); // 戻り値の型は Integer
Javaジェネリクスメソッドのポイントは、
「引数と戻り値の型の関係を、メソッドの型パラメータで表現できること」
です。
ここでは、「引数の List の要素型 T と、戻り値の型 T は同じである」という関係を
コンパイラに認識させています。
典型パターン2:上限境界付きジェネリクスメソッド
<T extends Number> で「Number 系だけ」に絞る
上限境界(extends)と組み合わせたジェネリクスメソッドは、
「特定の能力を持つ型だけを受け入れる」場面で使います。
例として、「配列の合計を計算する」メソッド。
public static <T extends Number> double sum(T[] array) {
double total = 0;
for (T n : array) {
total += n.doubleValue();
}
return total;
}
Javaここで <T extends Number> としたことで、
T は Number かそのサブクラスだけT は doubleValue() を必ず持っている
という前提で書けます。
使う側はこうです。
Integer[] ints = {1, 2, 3};
Double[] doubles = {1.5, 2.5};
double s1 = sum(ints); // OK
double s2 = sum(doubles); // OK
// String[] strs = {"a", "b"};
// double s3 = sum(strs); // コンパイルエラー(String は Number ではない)
Java「コンパイル時に、“このメソッドを呼んでいい型”を縛れる」
これが上限境界付きジェネリクスメソッドの強みです。
比較可能なものだけ受け付ける <T extends Comparable<T>>
さらに一歩進んだ例として、「最大値を返す」メソッドを考えます。
値どうしを比べるには compareTo が必要なので、
「T は Comparable<T> を実装している型だけ」にしたい。
public static <T extends Comparable<T>> T max(T a, T b) {
return (a.compareTo(b) >= 0) ? a : b;
}
Java使う側はこうです。
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ここでも、
<T extends Comparable<T>> によって、
「compareTo を持っている型だけが T になれる」という制約を表現しています。
ジェネリクスメソッド+上限境界は、「インターフェース的な能力」を前提にしたユーティリティを書くときに非常に強力です。
典型パターン3:ワイルドカードと組み合わせるジェネリクスメソッド
コピー関数で見る <? extends T> と <? super T>
PECS 原則のところで出した「コピー関数」は、
ジェネリクスメソッド+ワイルドカードの良い例です。
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ここでは、
<T> が「このメソッド全体で共有される型パラメータ」List<? extends T> が「T を生産する側(Producer)」List<? super T> が「T を消費する側(Consumer)」
という役割に分かれています。
T は「src と dest の間を流れる値の“共通の型”」です。<? extends T> / <? super T> は、「T とサブクラス/スーパークラスの許容範囲」を表現しています。
ジェネリクスメソッドにすることで、
「src と dest に関係した、1 つの T」
をコード上でちゃんと関連付けているのがポイントです。
ジェネリクスメソッドを書くときの考え方
「何と何が“同じ型”であってほしいか」をはっきりさせる
ジェネリクスメソッドの本質は、
「引数・戻り値・ローカル変数の“型の関係”を、型パラメータで明示すること」
です。
例えば first の例なら、
引数の List の要素型 T
戻り値の T
が「同じ型であってほしい」という関係です。
コピーの例なら、
src の要素型
dest に add する要素型
が「共通の T」として結び付いています。
メソッドを書くときに、
この引数とあの引数は、同じ型である必要があるか?
この戻り値の型は、どの引数の型と結び付いているか?
を先に言葉で整理してみて、
その関係を <T>, <K, V> などで表現していくイメージです。
どこまでをワイルドカードに任せ、どこから T を導入するか
型パラメータ <T> を使わずに、ワイルドカードだけで書ける場合もあります。
例えば、単に中身を表示するだけなら、こうで十分です。
public static void printAll(List<?> list) {
for (Object e : list) {
System.out.println(e);
}
}
Javaここには「リストの要素型どうしの関係」は特にありません。
「どんな List でもいいから、中身を Object として見せて」と言っているだけなので、
わざわざ <T> を導入する必要がない。
一方、「引数 2 つが同じ型であること」や「戻り値の型を引数から決めたい」といった関係があるなら、
そこには素直に <T> を導入した方が、コードを読む人に意図が伝わりやすくなります。
まとめ:ジェネリクスメソッドを自分の中でこう位置づける
ジェネリクスメソッドを一文でまとめると、
「メソッド単位で <T> などの型パラメータを宣言し、そのメソッド内の“型の関係”を静的に保証する仕組み」
です。
押さえておきたいポイントは、次のあたりです。
戻り値の型の“前”に <T> と書くのが「ジェネリクスメソッドの宣言」。
クラス全体をジェネリクスにしなくても、そのメソッドだけ汎用化できる。
引数と戻り値が「同じ型 T」であることをコードで表現できる。<T extends 〜> と組み合わせれば、「この能力を持つ型だけ」という制約も表現できる。
ワイルドカード(? extends, ? super)と組み合わせると、PECS に沿った柔軟な API が書ける。
