Java / LESSON 15 / JV15

Java:ジェネリクス

1学習の目的

2基礎解説

JV12で List<String> と書きました。この <>ジェネリクスです。「このListには String を入れる」と宣言することで、他の型を入れようとするとコンパイルエラーになります。

記号慣習的な意味使う場面
TType(型)一般的な型
EElement(要素)コレクション
K, VKey, ValueMapのような対
<T extends Number>型を制限する数値限定など
✅ 覚えるべき重要ポイント
  1. class Box<T> と書くと、T が「後から決まる型」になる。使う側が new Box<String>(…) と指定すれば、そのBoxは String 専用になる。
  2. キャストが不要になるのが最大の利点。List<String> から取り出した値は最初から String——Object で受けていた時代の (String) list.get(0) が要らない。
  3. メソッドにも付けられるstatic <T> T first(List<T> list) のように、戻り値の型の前<T> を書く。
  4. 型引数は複数持てるclass Pair<K, V> なら2つの型を扱える——Map がキーと値で2つ持つのと同じ仕組み。
  5. 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問)

JV15-D01 ジェネリックなクラス Box<T> を作れ(値を1つ保持し、get で取り出す)。String と Integer で1つずつ作り、2行出力すること。 コーディング★☆☆無料
期待される結果

出力2行。文字列 / 42

ヒント

class Box { private T content; ... } と書く。

模範解答
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用と別々にクラスを作る必要がない。

JV15-D02 ジェネリクスの最大の利点はどれか。
A. 実行が速い B. 型が保証されキャストが不要 C. メモリ節約 D. コードが短い
選択★☆☆無料
期待される結果

解答 == B

ヒント

取り出した値の型が分かっている。

模範解答
B
解説

コンパイル時に型が検査されるList<String> に数値を入れようとすれば、実行前にエラーで止められる。

JV15-D03 ジェネリックなメソッド firstOrNull を作れ(Listの先頭を返し、空なら null)。文字列リストと空リストで試し、2行出力すること。 コーディング★☆☆無料
期待される結果

出力2行。A / null

ヒント

static T firstOrNull(List list) の形。 は戻り値の型の前に書く。

模範解答
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 が戻り値の型。

JV15-D04 ジェネリックなクラスの型引数を補ってコードを完成させよ。 穴埋め★☆☆無料
コード
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 という慣習に従うと、他の人が読んだときに意図が伝わりやすい。

JV15-D05 List<String> に数値を入れようとするとどうなるか。
A. 実行時エラー B. コンパイルエラー C. 自動変換 D. 無視される
選択★☆☆無料
期待される結果

解答 == B

ヒント

型チェックはいつ行われるか。

模範解答
B
解説

実行前に止められるのがジェネリクスの価値。実行時に初めて分かるエラーより、はるかに発見が早い。

JV15-D06 型引数が2つのクラス Pair<K, V> を作れ(キーと値を保持し、toString で「在庫=5」の形式)。 コーディング★★☆広告解放
期待される結果

出力 == 在庫=5

ヒント

class Pair { private K key; private V value; ... }

模範解答
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 に置き換わる。

JV15-D07 型を数値に制限するコードを完成させよ。 穴埋め★★☆広告解放
コード
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() は呼べない。

JV15-D08 <T extends Number> と書く理由はどれか。
A. 速くなる B. 数値型に限定し、Numberのメソッドを使えるようにする C. 継承のため D. 必須だから
選択★★☆広告解放
期待される結果

解答 == B

ヒント

制限することで得られるものがある。

模範解答
B
解説

制限は「できることを増やす」。何でも受け付ける T では Object のメソッドしか呼べないが、Number に限定すれば数値演算ができる。

JV15-D09 境界つきジェネリクスを使い、数値リストの合計を返す sum を作れ(整数リストと小数リストの両方で試し、2行出力)。 コーディング★★☆広告解放
期待される結果

出力2行。6.0 / 4.0

ヒント

で宣言し、doubleValue() で合計する。

模範解答
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版…と増え続ける。

JV15-D10 ジェネリックなクラスにメソッドを追加せよ(Box<T> に、中身が null かを判定する isEmpty を作る)。値ありと null で試し、2行出力すること。 コーディング★★☆広告解放
期待される結果

出力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問)

JV15-S01 【汎用の最大値】<T extends Comparable<T>> を使い、リストの最大値を返すメソッドを作れ(整数リストと文字列リストで試し、2行出力)。 コーディング★★☆広告解放
期待される結果

出力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 を実装した型なら何でも比較できる。

JV15-S02 【型安全な保管庫】Box<T> を使い、商品オブジェクトを保管して取り出せ(Product クラスを作り、名前を出力)。 コーディング★★☆広告解放
期待される結果

出力 == ノートPC

ヒント

Box として使う。取り出した値はキャスト不要で Product 型。

模範解答
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 として使える。キャストを書かなくてよいので、型を間違えて実行時エラーになることがない。

JV15-S03 【ペアのリスト】Pair<String, Integer> のリストを作り、3件を「ノートPC=5」の形式で出力せよ。 コーディング★★☆広告解放
期待される結果

出力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>> は「文字列と数値の組のリスト」という意味になる。

JV15-S04 【Result型】成功と失敗を表す Result<T> を作れ(okfail の静的メソッドを持つ)。成功と失敗の2通りで試し、2行出力すること。 コーディング★★★広告解放
期待される結果

出力2行。100 / エラー発生

ヒント

static Result ok(T value) の形で作る。isOk() で判定する。

模範解答
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());
    }
}
解説

例外を使わずに失敗を表現できる。戻り値の型に「失敗するかもしれない」ことが現れるので、呼び出し側が対処を忘れにくい。

JV15-S05 【フィルタの汎用化】リストから条件に合うものを数える汎用メソッドを作れ(<T>List<T> を使い、null でない要素の数を返す)。 コーディング★★★広告解放
期待される結果

出力 == 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仕上げ課題

JV15-FINAL 【型安全な在庫管理】ジェネリクスを使い、再利用できる部品を作れ。

① 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 を受け取れるよう で宣言する。注文は ノートPC×2(成功)、マウス×0(数量不正)、モニター×1(在庫不足)。

模範解答
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の書き方に触れる。