はじめに

Javaでコレクションを使用する際、ArrayList<String>HashMap<String, Integer>のように<>で型を指定したことがあるでしょう。この仕組みが**ジェネリクス(Generics)**です。

ジェネリクスを使うことで、コンパイル時に型の不整合を検出でき、実行時エラーを未然に防げます。また、同じロジックを異なる型に対して再利用できるため、コードの保守性と可読性が向上します。

この記事では、ジェネリクスの基本概念から、ジェネリッククラス・メソッドの定義方法、境界型パラメータ、ワイルドカード、型消去の仕組みまで、段階的に解説します。

ジェネリクスとは何か

ジェネリクスとは、クラスやメソッドに型をパラメータとして渡す仕組みです。Java 5で導入され、型安全性を高めながらコードの再利用性を向上させる機能を提供します。

ジェネリクスがない場合の問題

ジェネリクス導入前のJavaでは、コレクションに格納する型を指定できませんでした。

1
2
3
4
5
6
7
8
// Java 1.4以前のスタイル(非推奨)
List list = new ArrayList();
list.add("文字列");
list.add(100); // 異なる型も追加できてしまう

// 取り出す際にキャストが必要
String str = (String) list.get(0); // OK
String num = (String) list.get(1); // 実行時にClassCastException

この方法には以下の問題があります。

問題点 説明
型安全性の欠如 どんな型でも格納できてしまう
キャストが必要 取り出すたびにキャストが必要で冗長
実行時エラー 型の不整合が実行時まで検出されない

ジェネリクスを使った解決

ジェネリクスを使うと、コンパイル時に型をチェックできます。

1
2
3
4
5
6
7
// ジェネリクスを使用
List<String> list = new ArrayList<>();
list.add("文字列");
// list.add(100); // コンパイルエラー!

// キャスト不要で取得
String str = list.get(0);

この方法のメリットは以下の通りです。

メリット 説明
型安全性 指定した型以外は格納できない
コンパイル時検査 型の不整合をコンパイル時に検出
キャスト不要 明示的なキャストが不要になる
コード可読性 どの型を扱うか一目で分かる

ジェネリクスの基本構文

ジェネリクスでは、<>(ダイヤモンド演算子)の中に型パラメータを指定します。

flowchart LR
    A["List&lt;String&gt;"] --> B["型パラメータ: String"]
    C["Map&lt;String, Integer&gt;"] --> D["キー: String"]
    C --> E["値: Integer"]

一般的に使用される型パラメータの命名規約は以下の通りです。

パラメータ 意味 使用例
E Element(要素) List<E>, Set<E>
K Key(キー) Map<K, V>
V Value(値) Map<K, V>
T Type(型) Box<T>, Pair<T>
N Number(数値) Calculator<N>
S, U 2番目、3番目の型 Pair<S, U>

ジェネリッククラスの定義

独自のジェネリッククラスを定義する方法を見ていきましょう。

基本的なジェネリッククラス

任意の型を格納できるシンプルなコンテナクラスを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Box<T> {
    private T content;
    
    public void set(T content) {
        this.content = content;
    }
    
    public T get() {
        return content;
    }
    
    public boolean isEmpty() {
        return content == null;
    }
}

このクラスを使用する際に、具体的な型を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// String型のBox
Box<String> stringBox = new Box<>();
stringBox.set("Hello Generics");
String message = stringBox.get(); // キャスト不要

// Integer型のBox
Box<Integer> intBox = new Box<>();
intBox.set(42);
int number = intBox.get();

// 独自クラスも使用可能
Box<User> userBox = new Box<>();
userBox.set(new User("田中", 25));
User user = userBox.get();

複数の型パラメータ

複数の型パラメータを持つクラスも定義できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Pair<K, V> {
    private final K key;
    private final V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() {
        return key;
    }
    
    public V getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return "Pair{" + key + " = " + value + "}";
    }
}

使用例は以下の通りです。

1
2
3
4
5
6
Pair<String, Integer> score = new Pair<>("数学", 85);
System.out.println(score.getKey());   // 数学
System.out.println(score.getValue()); // 85

Pair<Integer, String> lookup = new Pair<>(1001, "商品A");
System.out.println(lookup); // Pair{1001 = 商品A}

ジェネリッククラスと継承

ジェネリッククラスは通常のクラスと同様に継承できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 基底クラス
public class Container<T> {
    protected T item;
    
    public void store(T item) {
        this.item = item;
    }
    
    public T retrieve() {
        return item;
    }
}

// 型パラメータを維持して継承
public class ValidatedContainer<T> extends Container<T> {
    @Override
    public void store(T item) {
        if (item == null) {
            throw new IllegalArgumentException("nullは格納できません");
        }
        super.store(item);
    }
}

// 型パラメータを具体化して継承
public class StringContainer extends Container<String> {
    public int getLength() {
        return item != null ? item.length() : 0;
    }
}

ジェネリックメソッドの定義

クラス全体ではなく、メソッド単位でジェネリクスを使用することもできます。

基本的なジェネリックメソッド

メソッドの戻り値型の直前に型パラメータを宣言します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ArrayUtils {
    
    // 配列の最初の要素を取得
    public static <T> T getFirst(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[0];
    }
    
    // 配列の最後の要素を取得
    public static <T> T getLast(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[array.length - 1];
    }
    
    // 2つの要素を入れ替え
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}

使用する際は、コンパイラが型を推論します。

1
2
3
4
5
6
7
8
9
String[] names = {"Alice", "Bob", "Charlie"};
String first = ArrayUtils.getFirst(names); // Alice
String last = ArrayUtils.getLast(names);   // Charlie

Integer[] numbers = {10, 20, 30};
Integer firstNum = ArrayUtils.getFirst(numbers); // 10

// 明示的に型を指定することも可能
String explicit = ArrayUtils.<String>getFirst(names);

複数の型パラメータを持つメソッド

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class CollectionUtils {
    
    // 2つのリストをペアのリストに変換
    public static <K, V> List<Pair<K, V>> zip(List<K> keys, List<V> values) {
        List<Pair<K, V>> result = new ArrayList<>();
        int size = Math.min(keys.size(), values.size());
        
        for (int i = 0; i < size; i++) {
            result.add(new Pair<>(keys.get(i), values.get(i)));
        }
        return result;
    }
    
    // MapをPairのリストに変換
    public static <K, V> List<Pair<K, V>> mapToList(Map<K, V> map) {
        return map.entrySet()
                  .stream()
                  .map(e -> new Pair<>(e.getKey(), e.getValue()))
                  .toList();
    }
}

境界型パラメータ(extends)

型パラメータに制約を設けることで、特定のメソッドやプロパティにアクセスできるようになります。

上限境界(extends)

extendsキーワードを使って、型パラメータに上限を設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Numberまたはそのサブクラスのみ受け付ける
public class NumberBox<T extends Number> {
    private T value;
    
    public NumberBox(T value) {
        this.value = value;
    }
    
    public double doubleValue() {
        // Number型のメソッドを使用可能
        return value.doubleValue();
    }
    
    public int intValue() {
        return value.intValue();
    }
    
    public T getValue() {
        return value;
    }
}

使用例は以下の通りです。

1
2
3
4
5
6
7
8
NumberBox<Integer> intBox = new NumberBox<>(42);
System.out.println(intBox.doubleValue()); // 42.0

NumberBox<Double> doubleBox = new NumberBox<>(3.14);
System.out.println(doubleBox.intValue()); // 3

// String型は使用不可(コンパイルエラー)
// NumberBox<String> stringBox = new NumberBox<>("text");

型階層を図で表すと以下のようになります。

flowchart TB
    Number["Number"]
    Integer["Integer"]
    Double["Double"]
    Long["Long"]
    Float["Float"]
    
    Number --> Integer
    Number --> Double
    Number --> Long
    Number --> Float
    
    style Number fill:#e1f5fe

複数の境界

型パラメータに複数の制約を設定できます。クラスは1つ、インターフェースは複数指定可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Comparableを実装したNumber型のみ受け付ける
public class ComparableBox<T extends Number & Comparable<T>> {
    private T value;
    
    public ComparableBox(T value) {
        this.value = value;
    }
    
    public boolean isGreaterThan(T other) {
        // Comparableのメソッドを使用
        return value.compareTo(other) > 0;
    }
    
    public double toDouble() {
        // Numberのメソッドを使用
        return value.doubleValue();
    }
}
1
2
3
4
ComparableBox<Integer> box = new ComparableBox<>(10);
System.out.println(box.isGreaterThan(5));  // true
System.out.println(box.isGreaterThan(15)); // false
System.out.println(box.toDouble());        // 10.0

境界型パラメータを使ったメソッド

比較やソートを行うメソッドでよく使用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MathUtils {
    
    // 配列の最大値を取得
    public static <T extends Comparable<T>> T max(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(max) > 0) {
                max = array[i];
            }
        }
        return max;
    }
    
    // 配列の最小値を取得
    public static <T extends Comparable<T>> T min(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        
        T min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(min) < 0) {
                min = array[i];
            }
        }
        return min;
    }
    
    // 値が範囲内かチェック
    public static <T extends Comparable<T>> boolean isInRange(T value, T min, T max) {
        return value.compareTo(min) >= 0 && value.compareTo(max) <= 0;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Integer[] numbers = {3, 1, 4, 1, 5, 9, 2, 6};
System.out.println(MathUtils.max(numbers)); // 9
System.out.println(MathUtils.min(numbers)); // 1

String[] names = {"Bob", "Alice", "Charlie"};
System.out.println(MathUtils.max(names)); // Charlie
System.out.println(MathUtils.min(names)); // Alice

System.out.println(MathUtils.isInRange(5, 1, 10)); // true
System.out.println(MathUtils.isInRange(15, 1, 10)); // false

ワイルドカード(?、? extends、? super)

ワイルドカードは、メソッドの引数などで柔軟に型を受け付けるための仕組みです。

非境界ワイルドカード(?)

?は「任意の型」を表します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class WildcardExample {
    
    // 任意のリストの要素数を取得
    public static int countElements(List<?> list) {
        return list.size();
    }
    
    // 任意のリストを表示
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
}
1
2
3
4
5
6
7
8
List<String> strings = List.of("A", "B", "C");
List<Integer> numbers = List.of(1, 2, 3);

System.out.println(WildcardExample.countElements(strings)); // 3
System.out.println(WildcardExample.countElements(numbers)); // 3

WildcardExample.printList(strings); // A, B, C
WildcardExample.printList(numbers); // 1, 2, 3

ただし、List<?>には要素を追加できません(nullを除く)。

1
2
3
4
5
public static void addElement(List<?> list) {
    // list.add("text"); // コンパイルエラー
    // list.add(123);    // コンパイルエラー
    list.add(null);      // nullのみ追加可能
}

上限境界ワイルドカード(? extends)

? extends Tは「Tまたはそのサブクラス」を表します。プロデューサー(データを読み取る側)として使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class BoundedWildcard {
    
    // Number型またはそのサブクラスのリストから合計を計算
    public static double sum(List<? extends Number> numbers) {
        double sum = 0;
        for (Number n : numbers) {
            sum += n.doubleValue();
        }
        return sum;
    }
    
    // すべての要素をコピー(読み取り専用)
    public static <T> void copy(List<? extends T> src, List<T> dest) {
        for (T item : src) {
            dest.add(item);
        }
    }
}
1
2
3
4
5
6
7
List<Integer> integers = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
List<Long> longs = List.of(100L, 200L, 300L);

System.out.println(BoundedWildcard.sum(integers)); // 6.0
System.out.println(BoundedWildcard.sum(doubles));  // 7.5
System.out.println(BoundedWildcard.sum(longs));    // 600.0

下限境界ワイルドカード(? super)

? super Tは「Tまたはそのスーパークラス」を表します。コンシューマー(データを書き込む側)として使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class LowerBoundedWildcard {
    
    // Integer型またはその親クラスのリストに追加
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    // T型またはその親クラスのリストに要素を追加
    public static <T> void fill(List<? super T> list, T value, int count) {
        for (int i = 0; i < count; i++) {
            list.add(value);
        }
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

LowerBoundedWildcard.addNumbers(integers);
LowerBoundedWildcard.addNumbers(numbers);
LowerBoundedWildcard.addNumbers(objects);

System.out.println(integers); // [1, 2, 3]
System.out.println(numbers);  // [1, 2, 3]
System.out.println(objects);  // [1, 2, 3]

PECS原則

PECSは「Producer Extends, Consumer Super」の略で、ワイルドカードの使い分け指針です。

flowchart LR
    subgraph "Producer(読み取り)"
        PE["? extends T"]
        PE --> Read["データを取り出す"]
    end
    
    subgraph "Consumer(書き込み)"
        CS["? super T"]
        CS --> Write["データを追加する"]
    end
目的 ワイルドカード
データを読み取る ? extends T List<? extends Number>
データを書き込む ? super T List<? super Integer>
両方行う T(型パラメータ) List<T>

実践的な例を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class PecsExample {
    
    // srcから読み取り、destに書き込む
    public static <T> void transfer(
            List<? extends T> src,  // Producer(読み取り元)
            List<? super T> dest    // Consumer(書き込み先)
    ) {
        for (T item : src) {
            dest.add(item);
        }
    }
}
1
2
3
4
5
List<Integer> source = List.of(1, 2, 3);
List<Number> destination = new ArrayList<>();

PecsExample.transfer(source, destination);
System.out.println(destination); // [1, 2, 3]

型消去(Type Erasure)の理解

ジェネリクスはコンパイル時のみ有効で、実行時には型情報が消去されます。この仕組みを型消去と呼びます。

型消去の仕組み

コンパイラはジェネリクスの型パラメータを境界型(なければObject)に置き換えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// コンパイル前
public class Box<T> {
    private T content;
    public void set(T content) { this.content = content; }
    public T get() { return content; }
}

// 型消去後(実行時のバイトコード)
public class Box {
    private Object content;
    public void set(Object content) { this.content = content; }
    public Object get() { return content; }
}

境界型がある場合は、その型に置き換えられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// コンパイル前
public class NumberBox<T extends Number> {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}

// 型消去後
public class NumberBox {
    private Number value;
    public double doubleValue() { return value.doubleValue(); }
}

型消去の影響

型消去によって、いくつかの制約が発生します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TypeErasureExample {
    
    // 1. プリミティブ型は使用できない
    // Box<int> intBox; // コンパイルエラー
    Box<Integer> intBox; // ラッパークラスを使用
    
    // 2. 実行時に型パラメータの判定ができない
    public static <T> void checkType(List<T> list) {
        // if (list instanceof List<String>) {} // コンパイルエラー
        if (list instanceof List<?>) {
            System.out.println("これはリストです");
        }
    }
    
    // 3. 型パラメータのインスタンス生成ができない
    public static <T> T createInstance() {
        // return new T(); // コンパイルエラー
        return null;
    }
    
    // 4. 型パラメータの配列生成ができない
    public static <T> T[] createArray(int size) {
        // return new T[size]; // コンパイルエラー
        return null;
    }
}

型消去とオーバーロード

型消去により、ジェネリクスを使ったオーバーロードには注意が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class OverloadExample {
    // 以下の2つのメソッドは同じシグネチャになりコンパイルエラー
    
    // public void process(List<String> list) { }
    // public void process(List<Integer> list) { } // コンパイルエラー
    
    // 解決策1: メソッド名を変える
    public void processStrings(List<String> list) { }
    public void processIntegers(List<Integer> list) { }
    
    // 解決策2: ジェネリックメソッドを使う
    public <T> void process(List<T> list, Class<T> type) { }
}

ブリッジメソッド

ジェネリッククラスを継承してメソッドをオーバーライドすると、コンパイラはブリッジメソッドを生成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Node<T> {
    private T data;
    
    public void setData(T data) {
        this.data = data;
    }
}

public class StringNode extends Node<String> {
    @Override
    public void setData(String data) {
        System.out.println("文字列をセット: " + data);
        super.setData(data);
    }
}

コンパイラが生成するブリッジメソッドを含めると以下のようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class StringNode extends Node<String> {
    // オーバーライドしたメソッド
    @Override
    public void setData(String data) {
        System.out.println("文字列をセット: " + data);
        super.setData(data);
    }
    
    // コンパイラが生成するブリッジメソッド
    // @Override
    // public void setData(Object data) {
    //     setData((String) data);
    // }
}

ジェネリクスの制約と注意点

ジェネリクスを使用する際に知っておくべき制約と注意点をまとめます。

プリミティブ型は使用できない

ジェネリクスではプリミティブ型を直接使用できません。ラッパークラスを使用します。

1
2
3
4
5
6
7
// コンパイルエラー
// List<int> numbers = new ArrayList<>();

// ラッパークラスを使用
List<Integer> numbers = new ArrayList<>();
numbers.add(1);   // オートボクシング
int n = numbers.get(0);  // オートアンボクシング
プリミティブ型 ラッパークラス
int Integer
long Long
double Double
boolean Boolean
char Character

ジェネリック型の配列は作成できない

型安全性の観点から、ジェネリック型の配列は直接作成できません。

1
2
3
4
5
6
7
8
9
// コンパイルエラー
// List<String>[] array = new ArrayList<String>[10];

// 回避策1: 非境界ワイルドカードを使う
List<?>[] array = new ArrayList<?>[10];
array[0] = new ArrayList<String>();

// 回避策2: Listを使う
List<List<String>> listOfLists = new ArrayList<>();

static文脈での型パラメータ

クラスの型パラメータはstaticメンバーでは使用できません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Container<T> {
    // staticフィールドでは使用不可
    // private static T instance; // コンパイルエラー
    
    // staticメソッドでも使用不可
    // public static T getInstance() { } // コンパイルエラー
    
    // staticメソッド独自の型パラメータは使用可能
    public static <U> U createInstance(Class<U> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
}

例外クラスはジェネリックにできない

Throwableを継承したクラスに型パラメータを使用することはできません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// コンパイルエラー
// public class MyException<T> extends Exception { }

// 通常のクラスとして定義する
public class MyException extends Exception {
    private final Object detail;
    
    public MyException(String message, Object detail) {
        super(message);
        this.detail = detail;
    }
    
    @SuppressWarnings("unchecked")
    public <T> T getDetail() {
        return (T) detail;
    }
}

ヒープ汚染(Heap Pollution)

可変長引数とジェネリクスを組み合わせると、ヒープ汚染の警告が発生することがあります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class HeapPollutionExample {
    
    // 可変長引数とジェネリクス
    @SafeVarargs  // 安全であることを保証
    public static <T> List<T> asList(T... elements) {
        return Arrays.asList(elements);
    }
    
    // 危険な例(やってはいけない)
    public static void dangerousMethod(List<String>... lists) {
        Object[] array = lists;
        array[0] = Arrays.asList(42);  // ヒープ汚染
        String s = lists[0].get(0);    // ClassCastException
    }
}

@SafeVarargsアノテーションは、メソッドが型安全であることを宣言する場合に使用します。

実践的なジェネリクスの活用例

学んだ内容を活用した実践的な例を紹介します。

汎用的なキャッシュクラス

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Cache<K, V> {
    private final Map<K, V> store = new HashMap<>();
    private final Map<K, Long> timestamps = new HashMap<>();
    private final long ttlMillis;
    
    public Cache(long ttlMillis) {
        this.ttlMillis = ttlMillis;
    }
    
    public void put(K key, V value) {
        store.put(key, value);
        timestamps.put(key, System.currentTimeMillis());
    }
    
    public Optional<V> get(K key) {
        if (!store.containsKey(key)) {
            return Optional.empty();
        }
        
        long storedTime = timestamps.get(key);
        if (System.currentTimeMillis() - storedTime > ttlMillis) {
            // TTL超過
            store.remove(key);
            timestamps.remove(key);
            return Optional.empty();
        }
        
        return Optional.of(store.get(key));
    }
    
    public void evict(K key) {
        store.remove(key);
        timestamps.remove(key);
    }
    
    public void clear() {
        store.clear();
        timestamps.clear();
    }
}
1
2
3
4
5
6
// 5秒のTTLを持つキャッシュ
Cache<String, User> userCache = new Cache<>(5000);
userCache.put("user001", new User("田中", 25));

Optional<User> user = userCache.get("user001");
user.ifPresent(u -> System.out.println(u.getName())); // 田中

ジェネリックなResultクラス

処理結果を成功/失敗で表現するクラスです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public sealed interface Result<T> permits Result.Success, Result.Failure {
    
    record Success<T>(T value) implements Result<T> {}
    record Failure<T>(String error) implements Result<T> {}
    
    static <T> Result<T> success(T value) {
        return new Success<>(value);
    }
    
    static <T> Result<T> failure(String error) {
        return new Failure<>(error);
    }
    
    default boolean isSuccess() {
        return this instanceof Success;
    }
    
    default T getOrElse(T defaultValue) {
        return switch (this) {
            case Success<T> s -> s.value();
            case Failure<T> f -> defaultValue;
        };
    }
    
    default <U> Result<U> map(Function<T, U> mapper) {
        return switch (this) {
            case Success<T> s -> Result.success(mapper.apply(s.value()));
            case Failure<T> f -> Result.failure(f.error());
        };
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public Result<User> findUser(String id) {
    User user = userRepository.findById(id);
    if (user == null) {
        return Result.failure("ユーザーが見つかりません: " + id);
    }
    return Result.success(user);
}

// 使用例
Result<User> result = findUser("001");
String name = result
    .map(User::getName)
    .getOrElse("不明");

ビルダーパターンとジェネリクス

流暢なAPIを実現するためにジェネリクスを活用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class QueryBuilder<T> {
    private final Class<T> entityClass;
    private final List<String> conditions = new ArrayList<>();
    private String orderBy;
    private int limit = -1;
    
    private QueryBuilder(Class<T> entityClass) {
        this.entityClass = entityClass;
    }
    
    public static <T> QueryBuilder<T> from(Class<T> entityClass) {
        return new QueryBuilder<>(entityClass);
    }
    
    public QueryBuilder<T> where(String condition) {
        conditions.add(condition);
        return this;
    }
    
    public QueryBuilder<T> orderBy(String field) {
        this.orderBy = field;
        return this;
    }
    
    public QueryBuilder<T> limit(int limit) {
        this.limit = limit;
        return this;
    }
    
    public String build() {
        StringBuilder sql = new StringBuilder("SELECT * FROM ");
        sql.append(entityClass.getSimpleName());
        
        if (!conditions.isEmpty()) {
            sql.append(" WHERE ");
            sql.append(String.join(" AND ", conditions));
        }
        
        if (orderBy != null) {
            sql.append(" ORDER BY ").append(orderBy);
        }
        
        if (limit > 0) {
            sql.append(" LIMIT ").append(limit);
        }
        
        return sql.toString();
    }
}
1
2
3
4
5
6
7
8
String query = QueryBuilder.from(User.class)
    .where("age > 20")
    .where("status = 'ACTIVE'")
    .orderBy("name")
    .limit(10)
    .build();

// SELECT * FROM User WHERE age > 20 AND status = 'ACTIVE' ORDER BY name LIMIT 10

まとめ

この記事では、Javaジェネリクスの基本から応用まで解説しました。

項目 ポイント
ジェネリクスの目的 型安全性の確保とコードの再利用性向上
ジェネリッククラス class Box<T>のように型パラメータを定義
ジェネリックメソッド <T> T method()のようにメソッド単位で定義
境界型パラメータ <T extends Number>で型に制約を設ける
ワイルドカード ?? extends? superで柔軟な型受け入れ
PECS原則 Producer Extends, Consumer Super
型消去 実行時には型情報が消去される

ジェネリクスを適切に活用することで、型安全で再利用可能なコードを書けるようになります。コレクションフレームワークやStream APIを使う際にも、ジェネリクスの理解は不可欠です。

次のステップとして、ラムダ式やStream APIでのジェネリクスの活用方法を学んでいくことをおすすめします。

参考リンク