Java:ジェネリクス
1学習の目的
- <T> を使い、どんな型でも扱える部品を作れるようになる。JV12で使った List<String> の仕組みが理解できる。
- 型の安全性を保ったまま再利用する設計ができるようになる。Object で受けて毎回キャストする、という古い書き方から卒業する。
2基礎解説
JV12で List<String> と書きました。この <> がジェネリクスです。「このListには String を入れる」と宣言することで、他の型を入れようとするとコンパイルエラーになります。
| 記号 | 慣習的な意味 | 使う場面 |
|---|---|---|
| T | Type(型) | 一般的な型 |
| E | Element(要素) | コレクション |
| K, V | Key, Value | Mapのような対 |
| <T extends Number> | 型を制限する | 数値限定など |
- class Box<T> と書くと、T が「後から決まる型」になる。使う側が new Box<String>(…) と指定すれば、そのBoxは String 専用になる。
- キャストが不要になるのが最大の利点。List<String> から取り出した値は最初から String——Object で受けていた時代の (String) list.get(0) が要らない。
- メソッドにも付けられる。static <T> T first(List<T> list) のように、戻り値の型の前に <T> を書く。
- 型引数は複数持てる。class Pair<K, V> なら2つの型を扱える——Map がキーと値で2つ持つのと同じ仕組み。
- extends で型を制限できる。<T extends Number> なら数値系の型だけを受け付け、中で doubleValue() などが呼べるようになる。
現場使用例:コレクション全般、成功/失敗を表す Result 型、DBアクセスの共通処理、キャッシュの実装。ライブラリを作る側では必須の技術。
// ① ジェネリックなクラス
static class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T get() {
return content;
}
}
// 使う側が型を決める
Box<String> s = new Box<>("文字列");
Box<Integer> i = new Box<>(42);
String text = s.get(); // キャスト不要
// s.get() は必ず String と分かっている
// ② ジェネリックなメソッド(戻り値の型の前に <T>)
static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// ③ 型引数が2つ
static class Pair<K, V> {
private K key;
private V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
K getKey() { return key; }
V getValue() { return value; }
}
Pair<String, Integer> p = new Pair<>("在庫", 5);
// ④ 型を制限する(数値だけ受け付ける)
static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) {
total += n.doubleValue(); // Number のメソッドが使える
}
return total;
}
sum(List.of(1, 2, 3)); // 6.0
sum(List.of(1.5, 2.5)); // 4.0
// sum(List.of("A")); ← コンパイルエラー
3基本ドリル(10問)
出力2行。文字列 / 42
class Box
public class Main {
static class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T get() {
return content;
}
}
public static void main(String[] args) {
Box<String> s = new Box<>("文字列");
Box<Integer> i = new Box<>(42);
System.out.println(s.get());
System.out.println(i.get());
}
}1つのクラスで、どんな型でも扱える。String用・Integer用と別々にクラスを作る必要がない。
A. 実行が速い B. 型が保証されキャストが不要 C. メモリ節約 D. コードが短い 選択★☆☆無料
解答 == B
取り出した値の型が分かっている。
B
コンパイル時に型が検査される。List<String> に数値を入れようとすれば、実行前にエラーで止められる。
出力2行。A / null
static
import java.util.*;
public class Main {
static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
public static void main(String[] args) {
System.out.println(firstOrNull(List.of("A", "B")));
System.out.println(firstOrNull(new ArrayList<String>()));
}
}型引数は戻り値の型より前に書く。static <T> T の順序に注意——最初の <T> が宣言、次の T が戻り値の型。
public class Main {
static class Holder<____> {
private T value;
Holder(T value) { this.value = value; }
T get() { return value; }
}
public static void main(String[] args) {
System.out.println(new Holder<String>("テスト").get());
}
}出力 == テスト
Type を表す慣習的な1文字。
T
T は慣習であり、他の文字でもよい。ただし T・E・K・V という慣習に従うと、他の人が読んだときに意図が伝わりやすい。
A. 実行時エラー B. コンパイルエラー C. 自動変換 D. 無視される 選択★☆☆無料
解答 == B
型チェックはいつ行われるか。
B
実行前に止められるのがジェネリクスの価値。実行時に初めて分かるエラーより、はるかに発見が早い。
出力 == 在庫=5
class Pair
public class Main {
static class Pair<K, V> {
private K key;
private V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return key + "=" + value;
}
}
public static void main(String[] args) {
Pair<String, Integer> p = new Pair<>("在庫", 5);
System.out.println(p);
}
}Map もこの仕組みでできている。Map<String, Integer> の K と V が、それぞれ String と Integer に置き換わる。
import java.util.*;
public class Main {
static <T ____ Number> double sum(List<T> list) {
double total = 0;
for (T n : list) {
total += n.doubleValue();
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
}
}出力 == 6.0
型の上限を指定するキーワード(継承と同じ7文字)。
extends
extends で型を制限すると、その型のメソッドが使える。制限しなければ doubleValue() は呼べない。
A. 速くなる B. 数値型に限定し、Numberのメソッドを使えるようにする C. 継承のため D. 必須だから 選択★★☆広告解放
解答 == B
制限することで得られるものがある。
B
制限は「できることを増やす」。何でも受け付ける T では Object のメソッドしか呼べないが、Number に限定すれば数値演算ができる。
出力2行。6.0 / 4.0
import java.util.*;
public class Main {
static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) {
total += n.doubleValue();
}
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.5, 2.5)));
}
}Integer でも Double でも同じメソッドで扱える。もし型ごとにメソッドを作っていたら、int版・double版・long版…と増え続ける。
出力2行。false / true
return content == null; と書く。
public class Main {
static class Box<T> {
private T content;
Box(T content) {
this.content = content;
}
boolean isEmpty() {
return content == null;
}
}
public static void main(String[] args) {
System.out.println(new Box<String>("値").isEmpty());
System.out.println(new Box<String>(null).isEmpty());
}
}T が何であっても null チェックはできる。型に依存しない操作なら、制限なしの T のままで十分。
4実践シナリオ(5問)
出力2行。30 / C
compareTo で比較する。a.compareTo(b) > 0 なら a のほうが大きい。
import java.util.*;
public class Main {
static <T extends Comparable<T>> T max(List<T> list) {
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
public static void main(String[] args) {
System.out.println(max(List.of(10, 30, 20)));
System.out.println(max(List.of("A", "C", "B")));
}
}数値でも文字列でも同じメソッドで最大値が求まる。Comparable を実装した型なら何でも比較できる。
出力 == ノートPC
Box
public class Main {
static class Product {
String name;
Product(String name) { this.name = name; }
}
static class Box<T> {
private T content;
Box(T content) { this.content = content; }
T get() { return content; }
}
public static void main(String[] args) {
Box<Product> box = new Box<>(new Product("ノートPC"));
Product p = box.get();
System.out.println(p.name);
}
}取り出した瞬間から Product として使える。キャストを書かなくてよいので、型を間違えて実行時エラーになることがない。
出力3行
List
import java.util.*;
public class Main {
static class Pair<K, V> {
private K key;
private V value;
Pair(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return key + "=" + value;
}
}
public static void main(String[] args) {
List<Pair<String, Integer>> stocks = new ArrayList<>();
stocks.add(new Pair<>("ノートPC", 5));
stocks.add(new Pair<>("マウス", 42));
stocks.add(new Pair<>("モニター", 0));
for (Pair<String, Integer> p : stocks) {
System.out.println(p);
}
}
}ジェネリクスは入れ子にできる。List<Pair<String, Integer>> は「文字列と数値の組のリスト」という意味になる。
出力2行。100 / エラー発生
static
public class Main {
static class Result<T> {
private T value;
private String error;
private Result(T value, String error) {
this.value = value;
this.error = error;
}
static <T> Result<T> ok(T value) {
return new Result<>(value, null);
}
static <T> Result<T> fail(String error) {
return new Result<>(null, error);
}
boolean isOk() { return error == null; }
T getValue() { return value; }
String getError() { return error; }
}
public static void main(String[] args) {
Result<Integer> r1 = Result.ok(100);
Result<Integer> r2 = Result.fail("エラー発生");
System.out.println(r1.isOk() ? r1.getValue() : r1.getError());
System.out.println(r2.isOk() ? r2.getValue() : r2.getError());
}
}例外を使わずに失敗を表現できる。戻り値の型に「失敗するかもしれない」ことが現れるので、呼び出し側が対処を忘れにくい。
出力 == 2
リストをループし、null でないものを数える。
import java.util.*;
public class Main {
static <T> int countNotNull(List<T> list) {
int count = 0;
for (T item : list) {
if (item != null) {
count++;
}
}
return count;
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add(null);
list.add("B");
System.out.println(countNotNull(list));
}
}型に依存しない処理は T のままで書ける。String でも Integer でも Product でも、同じメソッドが使える。
5仕上げ課題
① Result<T> クラス
・static <T> Result<T> ok(T value) … 成功
・static <T> Result<T> fail(String error) … 失敗
・isOk()、getValue()、getError()
② Product クラス:name・price・stock(コンストラクタで受け取る)
③ 汎用メソッド
・static <T> T firstOrNull(List<T> list) … 先頭または null
・static <T extends Number> double sum(List<T> list) … 数値の合計
④ 業務メソッド
・static Result<Integer> order(Product p, int qty)
- 数量が0以下 → fail「数量が不正です」
- 在庫不足 → fail「在庫不足です」
- 成功 → ok(合計金額)
main の処理
① 商品リスト(3件)の先頭商品名を出力
② 在庫数のリストから合計を出力
③ 3通りの注文を処理して結果を出力(成功 / 数量不正 / 在庫不足)
データ:ノートPC/128000/5、マウス/3200/42、モニター/45000/0
期待される出力(5行)
先頭商品:ノートPC
在庫合計:47.0個
成功:256,000円
失敗:数量が不正です
失敗:在庫不足です 総合★★★広告解放
出力5行が完全一致
Result の value と error は private にし、静的メソッドから生成する。sum は List
import java.util.*;
public class Main {
static class Result<T> {
private T value;
private String error;
private Result(T value, String error) {
this.value = value;
this.error = error;
}
static <T> Result<T> ok(T value) {
return new Result<>(value, null);
}
static <T> Result<T> fail(String error) {
return new Result<>(null, error);
}
boolean isOk() { return error == null; }
T getValue() { return value; }
String getError() { return error; }
}
static class Product {
String name;
int price;
int stock;
Product(String name, int price, int stock) {
this.name = name;
this.price = price;
this.stock = stock;
}
}
static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) {
total += n.doubleValue();
}
return total;
}
static Result<Integer> order(Product p, int qty) {
if (qty <= 0) {
return Result.fail("数量が不正です");
}
if (qty > p.stock) {
return Result.fail("在庫不足です");
}
return Result.ok(p.price * qty);
}
public static void main(String[] args) {
List<Product> products = new ArrayList<>();
products.add(new Product("ノートPC", 128000, 5));
products.add(new Product("マウス", 3200, 42));
products.add(new Product("モニター", 45000, 0));
Product first = firstOrNull(products);
System.out.println("先頭商品:" + first.name);
List<Integer> stocks = new ArrayList<>();
for (Product p : products) {
stocks.add(p.stock);
}
System.out.println("在庫合計:" + sum(stocks) + "個");
int[][] requests = {{0, 2}, {1, 0}, {2, 1}};
for (int[] req : requests) {
Product p = products.get(req[0]);
Result<Integer> result = order(p, req[1]);
if (result.isOk()) {
System.out.println("成功:"
+ String.format("%,d", result.getValue()) + "円");
} else {
System.out.println("失敗:" + result.getError());
}
}
}
}Result<T> は、JV13の例外処理とは違うアプローチだ。例外は「異常事態」を表すのに対し、Result は「失敗も想定内の結果の1つ」として扱う。在庫不足は異常ではなく、日常的に起こることだ——だから例外ではなく戻り値で表現する。
Result<Integer> という型を見れば、「成功すると整数が返る、失敗するかもしれない」と一目で分かる。もし単に int を返していたら、失敗をどう表現するのか——-1 を返す? その値が本当にエラーなのか、それとも計算結果なのか、区別がつかなくなる。
<T extends Number> の制限にも意味がある。この宣言のおかげで n.doubleValue() が呼べる。もし単なる <T> だったら、Tが何か分からないので Object のメソッドしか使えない。制限することで、できることが増える——ジェネリクスの逆説的な性質だ。
ジェネリクスは「作る側」の技術だ。普段は List<String> のように使うだけだが、共通部品やライブラリを作る立場になると必須になる。今の段階では「Listの<>の正体が分かった」で十分だ。
次章ではラムダ式とStreamを学ぶ。ループを書かずにデータを処理する、現代的なJavaの書き方に触れる。