Javaにおけるジェネリクスの理解と実践

ジェネリクスとは、クラス、インターフェース、メソッドの定義時にデータ型をパラメータ(型パラメータ)として受け取る仕組みです。この型パラメータは通常のデータ型のように扱え、変数宣言(メンバ変数、ローカル変数、メソッドパラメータを含む)や戻り値の型指定に使用できます。実際に型パラメータに割り当てられるデータ型は、使用時に指定した型となります。

ジェネリクスはクラス、インターフェース、メソッドに適用でき、それぞれ「ジェネリッククラス」「ジェネリックインターフェース」「ジェネリックメソッド」と呼びます。

型パラメータが保持できるのはデータ型そのもののみであり、使用時にはデータ型自体を渡す必要があります。

型パラメータの宣言位置

  • クラス名やインターフェース名の直後に、<>内で宣言します。
  • メソッドの戻り値の型の直前に、<>内で宣言します。
  • 複数の型パラメータはカンマで区切ります。

型パラメータは通常のメソッドパラメータとは以下の点で異なります。

  • 仮パラメータの違い: 通常のメソッドの仮パラメータは本質的にローカル変数で、()内にデータ型と変数名を指定します。ジェネリクスの仮パラメータは変数の一種で、<>内に変数名のみ(通常は <T><E><V> など)を指定し、データ型は指定しません。
  • 実パラメータの違い: 通常のメソッドの実パラメータは、仮パラメータで指定されたデータ型の値またはオブジェクトである必要があります。ジェネリクスの実パラメータはデータ型そのものであり、StringInteger、カスタムクラスなどが該当します。

// ジェネリックインターフェース
public interface Generator<T> {
    public T next();
}
// ジェネリッククラス
public class Generic<T>{ 
    private T key;  // メンバ変数の型はT

    public Generic(T key) { // コンストラクタの仮パラメータもT型
        this.key = key;
    }

    public T getKey(){ // 戻り値の型はT
        return key;
    }
}
// ジェネリックメソッド(親クラス参照から子クラスオブジェクトを取得)
public <T> T getObj(Object args) {
    T t = (T) args;
    return t;
}
// ジェネリックメソッド(異なる要素型のListを同様に処理)
public <T> void printMsg(List<T> args) {
    for (T t : args) {
        System.out.println("型テスト: " + t);
    }
}

public void test() {
    ArrayList<String> l1 = new ArrayList<>();
    l1.add("aa");
    l1.add("bb");
    printMsg(l1);

    ArrayList<Integer> l2 = new ArrayList<>();
    l2.add(1);
    l2.add(2);
    printMsg(l2);
}

1. ジェネリクスの基本概念

ジェネリクスは「パラメータ化された型」とも呼ばれます。メソッドの仮パラメータと同様に、型そのものをパラメータとして受け取る仕組みです。これにより、データ型を変数のように扱い、使用時に具体的な型を指定できます。

ジェネリクスの本質は、型をパラメータ化することにあります。操作するデータ型をパラメータとして指定することで、クラス、インターフェース、メソッドで同一のコードを異なる型に対して安全に利用できます。

2. 実演例

よく挙げられる例として、以下のコードがあります。

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for (int i = 0; i < arrayList.size(); i++) {
    String item = (String) arrayList.get(i);
    System.out.println("要素: " + item);
}

このコードは実行時に以下のエラーで終了します。

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

ArrayListは任意の型を保持できるため、StringIntegerの両方を追加し、それらをすべてStringとしてキャストすると問題が発生します。ジェネリクスを使用することで、このような問題をコンパイル時に解決できます。

List<String> arrayList = new ArrayList<String>();
// arrayList.add(100);  // コンパイルエラーになる

3. 特徴

ジェネリクスはコンパイル時のみ有効です。以下のコードを確認してください。

List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();

Class classString = stringList.getClass();
Class classInteger = integerList.getClass();

if (classString.equals(classInteger)) {
    System.out.println("型は同じです");
}

出力結果: 型は同じです

コンパイル後、ジェネリクス情報は消去されます。つまり、Javaのジェネリクスはコンパイル時に型チェックを行うための仕組みであり、実行時にはジェネリクスに関する情報は保持されません。型パラメータは論理的に異なる複数の型に見えますが、実際にはすべて同じ基本型として扱われます。

4. ジェネリクスの使用法

4.1 ジェネリッククラス

ジェネリッククラスは、クラス定義時に型パラメータを使用します。代表的な例はListSetMapなどのコレクションクラスです。

// 基本的なジェネリッククラス
public class Generic<T>{ 
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
// 使用例
Generic<Integer> intGeneric = new Generic<Integer>(123456);
Generic<String> strGeneric = new Generic<String>("value");
System.out.println("key: " + intGeneric.getKey());
System.out.println("key: " + strGeneric.getKey());

出力:

key: 123456
key: value

ジェネリッククラスに型実パラメータを渡さない場合、Tは任意の型として扱われます。以下の例では、型実パラメータを指定していないため、異なる型の値を保持できます。

Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

System.out.println("key: " + generic.getKey());
System.out.println("key: " + generic1.getKey());
System.out.println("key: " + generic2.getKey());
System.out.println("key: " + generic3.getKey());

出力:

key: 111111
key: 4444
key: 55.55
key: false

注意点:

  • 型パラメータに指定できるのはクラス型のみであり、プリミティブ型は使用できません。
  • 特定のジェネリック型に対してinstanceofを使用することはできません。例: if (obj instanceof Generic<Number>) はコンパイルエラーになります。

4.2 ジェネリックインターフェース

ジェネリックインターフェースは、生産者(ジェネレーター)パターンでよく使用されます。

public interface Generator<T> {
    public T next();
}

実装クラスが型実パラメータを指定しない場合:

class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

実装クラスが型実パラメータを指定する場合:

public class FruitGenerator implements Generator<String> {

    private String[] fruits = {"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

4.3 ワイルドカード

IntegerNumberのサブクラスですが、Generic<Integer>Generic<Number>は異なる型として扱われます。そのため、Generic<Number>型の引数にGeneric<Integer>を渡すことはできません。この問題を解決するためにワイルドカード(?)を使用します。

public void showKeyValue1(Generic<?> obj){
    System.out.println("key: " + obj.getKey());
}

ワイルドカード?は型実パラメータの一種であり、任意の型を表します。型が確定していない場合や、型の具体的な機能を使用しない場合に有用です。

4.4 ジェネリックメソッド

ジェネリックメソッドは、メソッド呼び出し時に型実パラメータを指定します。ジェネリッククラスのメンバメソッドがすべてジェネリックメソッドというわけではありません。

public <T> T genericMethod(Class<T> tClass) throws InstantiationException, IllegalAccessException {
    T instance = tClass.newInstance();
    return instance;
}

4.4.1 基本例

public class GenericTest {
    public class Generic<T> {     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        // getKeyはジェネリッククラスの型パラメータTを使用するが、ジェネリックメソッドではない
        public T getKey(){
            return key;
        }
    }

    // 真のジェネリックメソッド: 独自の型パラメータを宣言
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key: " + container.getKey());
        T test = container.getKey();
        return test;
    }

    // ワイルドカードを使用した通常のメソッド
    public void showKeyValue2(Generic<?> obj){
        System.out.println("key: " + obj.getKey());
    }
}

重要な区別: 戻り値の型の直前に<T>と宣言されているメソッドのみがジェネリックメソッドです。ジェネリッククラス内のメソッドで型パラメータTを使用しても、それがクラスレベルで宣言されたものであればジェネリックメソッドではありません。

4.4.2 クラス内のジェネリックメソッド

public class GenerateTest<T> {
    public void show_1(T t){
        System.out.println(t.toString());
    }

    // Show_1とは異なる型パラメータEを持つジェネリックメソッド
    public <E> void show_3(E t){
        System.out.println(t.toString());
    }

    // 同名の型パラメータTを持つが、クラスのTとは独立
    public <T> void show_2(T t){
        System.out.println(t.toString());
    }
}

4.4.3 可変長引数との組み合わせ

public <T> void printMsg(T... args){
    for(T t : args){
        System.out.println("要素: " + t);
    }
}

// 呼び出し例
printMsg("111", 222, "aaaa", 2323.4, 55.55);

4.4.4 静的メソッドとジェネリクス

静的メソッドはクラスレベルの型パラメータにアクセスできません。静的メソッドでジェネリクスを使用するには、メソッド自体をジェネリックメソッドとして定義する必要があります。

public class StaticGenerator<T> {
    // 静的メソッドは独自の型パラメータを宣言する必要がある
    public static <T> void show(T t){
        System.out.println(t);
    }
}

4.4.5 まとめ

ジェネリックメソッドはクラスから独立して型を変更できるため、可能であればジェネリックメソッドを優先的に使用することが推奨されます。特に静的メソッドでジェネリクスを使用する場合は、必ずジェネリックメソッドとして定義する必要があります。

4.5 型パラメータの境界

型パラメータに制約を設けることができます。上限境界(extends)や下限境界(super)を指定できます。

上限境界の例: ? extends Numberは、Numberまたはそのサブクラスのみを受け入れます。

public void showKeyValue1(Generic<? extends Number> obj){
    System.out.println("key: " + obj.getKey());
}
// ジェネリッククラス自体に境界を設定
public class Generic<T extends Number>{
    private T key;
    // ...
}

ジェネリックメソッドにおける境界の指定:

public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key: " + container.getKey());
    T test = container.getKey();
    return test;
}

境界は型パラメータの宣言と同時に指定する必要があります。

4.6 ジェネリック配列

Javaでは、具体的なジェネリック型の配列を直接作成することはできません。

// コンパイルエラー
List<String>[] ls = new ArrayList<String>[10];

ワイルドカードを使用した場合は作成可能です。

List<?>[] ls = new ArrayList<?>[10];

以下も許容されます。

List<String>[] ls = new ArrayList[10];

ジェネリック配列が制限されている理由は、型消去によって実行時に型情報が失われるためです。以下の例で問題を示します。

List<String>[] lsa = new List[10];  // 警告は出るがコンパイル可能
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(3);    
oa[1] = li;  // 実行時チェックを通過
String s = lsa[1].get(0);  // 実行時エラー: ClassCastException

ワイルドカードを使用すると、明示的なキャストが必要になり、安全性が向上します。

List<?>[] lsa = new List<?>[10];
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(3);    
oa[1] = li;    
Integer i = (Integer) lsa[1].get(0);  // 明示的なキャストが必要

タグ: ジェネリクス Java 型パラメータ ジェネリッククラス ジェネリックメソッド

7月25日 19:24 投稿