Java アドバンス

Java ジェネリクス (Generics)

1. ジェネリクス(Generics)とは

ジェネリクス(Generics)を使用すると、具体的なデータ型を事前に指定することなく、さまざまなデータ型で動作するクラス、インターフェース、およびメソッドを作成できます。

これにより、コードの柔軟性再利用性、そして型安全性(Type Safety)が向上します。

2. なぜジェネリクスを使用するのか?

ジェネリクスを導入する主なメリットは以下の3点です。

  • コードの再利用性: 異なるデータ型に対して動作する単一のクラスやメソッドを記述できます。
  • 型安全性: 型のエラーをランタイム(実行時)ではなく、コンパイル時にキャッチできます。
  • クリーンなコード: オブジェクトを取得する際に、明示的なキャスト(型変換)を行う必要がありません。

3. ジェネリッククラスの例

ジェネリクスを使用して、異なるデータ型を扱えるクラスを作成できます。

// T は任意のデータ型のプレースホルダ
class Box<T> {
  T value; 

  void set(T value) {
    this.value = value;
  }

  T get() {
    return value;
  }
}

public class Main {
  public static void main(String[] args) {
    // Stringを保持するBoxを作成
    Box<String> stringBox = new Box<>();
    stringBox.set("こんにちは");
    System.out.println("値: " + stringBox.get());

    // Integerを保持するBoxを作成
    Box<Integer> intBox = new Box<>();
    intBox.set(50);
    System.out.println("値: " + intBox.get());
  }
}

3.1 コード解説

  • T はジェネリック型パラメータです。これはデータ型のプレースホルダのように機能します。
  • Box<String> を作成すると、TString になります。
  • Box<Integer> を作成すると、TInteger になります。
  • これにより、同じクラスをコードの書き直しなしで異なるデータ型に再利用できます。

4. ジェネリックメソッドの例

クラス全体だけでなく、任意のデータ型で動作するメソッドを作成することも可能です。

public class Main {
  // ジェネリックメソッド:任意の型 T で動作
  public static <T> void printArray(T[] array) {
    for (T item : array) {
      System.out.println(item);
    }
  }

  public static void main(String[] args) {
    // Stringの配列
    String[] names = {"ジェニー", "リアム"};

    // Integerの配列
    Integer[] numbers = {1, 2, 3};

    // 両方の配列に対してジェネリックメソッドを呼び出す
    printArray(names);
    printArray(numbers);
  }
}

4.1 コード解説

  • <T> はジェネリック型パラメータであり、このメソッドが String, Integer, Double など、あらゆる型で動作することを意味します。
  • メソッド printArray() は型 T の配列を受け取り、すべての要素を出力します。
  • メソッドを呼び出す際、Javaは渡された引数に基づいて T が何であるべきかを自動的に判別します。
  • これは、型ごとに同じようなコードを繰り返す代わりに、一つのメソッドで複数の型に対応したい場合に非常に有用です。

5. 境界付き型パラメータ (Bounded Types)

extends キーワードを使用すると、ジェネリッククラスやメソッドが受け入れる型を制限することができます。
例えば、型を Number クラスのサブクラスのみに制限したい場合は以下のように記述します。

class Stats<T extends Number> {
  T[] nums;

  // コンストラクタ
  Stats(T[] nums) {
    this.nums = nums;
  }

  // 平均を計算する
  double average() {
    double sum = 0;
    for (T num : nums) {
      // すべてのNumber型が持つ doubleValue() を使用
      sum += num.doubleValue();
    }
    return sum / nums.length;
  }
}

public class Main {
  public static void main(String[] args) {
    // Integerでの使用
    Integer[] intNums = {10, 20, 30, 40};
    Stats<Integer> intStats = new Stats<>(intNums);
    System.out.println("Integerの平均: " + intStats.average());

    // Doubleでの使用
    Double[] doubleNums = {1.5, 2.5, 3.5};
    Stats<Double> doubleStats = new Stats<>(doubleNums);
    System.out.println("Doubleの平均: " + doubleStats.average());
  }
}

補足:最初のケースで int 型の値を使用していますが、.doubleValue() メソッドがそれらを double に変換するため、結果は小数点付きで表示されます。

5.1 コード解説

  • <T extends Number>: T が Integer, Double, Float などの数値型のみで動作するように制限します。
  • .doubleValue(): 計算のために、あらゆる数値を double に変換します。
  • 要素が Number のサブクラスである限り、あらゆる数値配列に対して動作します。

6. ジェネリックコレクション

Javaの ArrayListHashMap などのコレクションフレームワークは、内部的にジェネリクスを使用しています。

ArrayList<String> list = new ArrayList<>();
list.add("リンゴ");
String fruit = list.get(0); // キャストは不要

7. まとめ

  • ジェネリクスは、コードに柔軟性と型安全性をもたらします。
  • T などのアルファベットを使用して、型のプレースホルダを定義します。
  • ジェネリクスは、クラス、メソッド、およびインターフェースに適用できます。
  • 境界(Bounds)を使用することで、許可される型を制限できます。