Javaのジェネリクス入門|型安全なプログラミング

先生

Javaジェネリクスをマスターして、型安全で効率的なコードを書こう!

Javaジェネリクスとは?基本を理解しよう

Javaのジェネリクスは、Java 5から導入された機能で、クラスやメソッドを定義する際に、型をパラメータ化できる仕組みです。これにより、コンパイル時の型安全性を高め、実行時のClassCastExceptionを減らすことができます。

ジェネリクスを使用することで、異なる型のオブジェクトを扱うクラスやメソッドを、型ごとに個別に作成する必要がなくなります。例えば、Listインターフェースを考えてみましょう。ジェネリクスがない場合、Object型のListを使用し、要素を取り出す際にキャストが必要でした。しかし、ジェネリクスを使うことで、List<String>やList<Integer>のように、格納する型を明示的に指定できるようになります。

これにより、コンパイラは格納する型をチェックし、型エラーをコンパイル時に検出できます。また、要素を取り出す際にキャストが不要になり、コードの可読性と安全性が向上します。

ジェネリクスの基本的な使い方

ジェネリクスは、クラス、インターフェース、メソッドで使用できます。ここでは、基本的な使い方を例を交えて説明します。

クラスをジェネリクスとして定義するには、クラス名の後ろに山括弧<>で囲まれた型パラメータを指定します。例えば、次のように記述します。

public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } }

この例では、Boxクラスは型パラメータTを持っています。Tは、Boxオブジェクトが保持する型を表します。Box<String>やBox<Integer>のように、具体的な型を指定してBoxクラスのインスタンスを作成できます。

Box<Integer> integerBox = new Box<>(); integerBox.set(10); Integer integer = integerBox.get(); // キャスト不要

メソッドをジェネリクスとして定義するには、戻り値の型よりも前に山括弧<>で囲まれた型パラメータを指定します。例えば、次のように記述します。

public <T> T printAndReturn(T input) { System.out.println(input); return input; }

この例では、printAndReturnメソッドは型パラメータTを持っています。Tは、引数inputと戻り値の型を表します。メソッドを呼び出す際に、引数の型からTの型が推論されます。

String result = printAndReturn("Hello, Generics!"); // TはStringと推論される

型パラメータの制限:上限境界と下限境界

ジェネリクスでは、型パラメータに制限を設けることができます。上限境界と下限境界の2種類があります。

上限境界は、型パラメータが特定のクラスまたはインターフェースのサブタイプでなければならないことを指定します。extendsキーワードを使用します。

public class NumberBox<T extends Number> { private T number; public NumberBox(T number) { this.number = number; } public double doubleValue() { return number.doubleValue(); } }

この例では、NumberBoxクラスの型パラメータTは、Numberクラスまたはそのサブクラス(Integer, Doubleなど)でなければなりません。これにより、numberフィールドに対してdoubleValue()メソッドを安全に呼び出すことができます。

下限境界は、型パラメータが特定のクラスまたはインターフェースのスーパークラスでなければならないことを指定します。superキーワードを使用します。

public void processElements(List<? super Integer> list) { list.add(10); }

この例では、processElementsメソッドの引数listは、Integerクラスまたはそのスーパークラス(Number, Objectなど)のリストでなければなりません。これにより、listにInteger型の要素を安全に追加できます。

ワイルドカード:? の使い方

ワイルドカード(?)は、ジェネリクスの型パラメータが不明な場合に使用します。上限境界と下限境界と同様に、? extends Tや? super Tのように使用できます。

List<? extends Number>は、Numberクラスまたはそのサブクラスのリストを表します。このリストから要素を取り出すことはできますが、要素を追加することはできません(nullは例外)。

public double sumOfList(List<? extends Number> list) { double sum = 0; for (Number n : list) { sum += n.doubleValue(); } return sum; }

この例では、sumOfListメソッドは、Numberクラスまたはそのサブクラスのリストを受け取り、要素の合計を計算します。

List<? super Integer>は、Integerクラスまたはそのスーパークラスのリストを表します。このリストにInteger型の要素を追加することはできますが、要素を取り出すとObject型になります。

public void addIntegers(List<? super Integer> list) { list.add(1); list.add(2); }

この例では、addIntegersメソッドは、Integerクラスまたはそのスーパークラスのリストを受け取り、Integer型の要素を追加します。

ジェネリクスの注意点

ジェネリクスを使用する際には、いくつかの注意点があります。

1. 型消去:Javaのジェネリクスはコンパイル時に型情報を消去します。そのため、実行時には型パラメータの情報は失われます。

2. 型パラメータのインスタンス化:型パラメータをnew演算子でインスタンス化することはできません。例えば、new T()はコンパイルエラーになります。

3. staticフィールドでの型パラメータの使用:staticフィールドでは型パラメータを使用できません。staticフィールドはクラス全体で共有されるため、特定の型パラメータに依存することができないためです。

4. プリミティブ型の使用:型パラメータにはプリミティブ型(int, doubleなど)を直接指定できません。ラッパークラス(Integer, Doubleなど)を使用する必要があります。

参考リンク

まとめ

Javaのジェネリクスは、型安全性を高め、コードの再利用性を向上させる強力な機能です。クラスやメソッドをジェネリクスとして定義することで、異なる型のオブジェクトを扱うコードを簡潔に記述できます。上限境界や下限境界、ワイルドカードを適切に使用することで、より柔軟な型指定が可能になります。ジェネリクスの注意点を理解し、適切に活用することで、より堅牢で保守性の高いJavaアプリケーションを開発できます。