0xe3aad

0xe3aad

Javaのジェネリクス

擦除法#

コンパイラは型<T>を Object として扱う#

public class Pair<T> {
    private final T first;
    private final T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }

    public T getLast() {
        return last;
    }
}

Java のジェネリクスはコンパイル時に行われ、コンパイラは上記のコードを擦除法で処理し、JVM が実際に見るコードは以下の通りです:

public class Pair {
    private final Object first;
    private final Object last;

    public Pair(Object first, Object last) {
        this.first = first;
        this.last = last;
    }

    public Object getFirst() {
        return first;
    }

    public Object getLast() {
        return last;
    }
}

コンパイラは<T>に基づいて安全なキャストを実現する#

Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();

コンパイラは T の型に基づいて安全にキャストします:

Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();

擦除法の限界#

  • <T>は基本型にはできません。なぜなら、擦除法はそれをObjectに置き換えるからです。
  • ジェネリクスを持つClassを取得することはできません。なぜなら、ジェネリクスはコンパイル時に処理されるため、取得されるのはすべてPair<Object>です。
    例えば:
@Test
void testGetClass() {
    Pair<String> p1 = new Pair<>("Hello", "World");
    Pair<Integer> p2 = new Pair<>(114, 514);
    Class c1 = p1.getClass();
    Class c2 = p2.getClass();

    assertEquals(c1, c2);
    assertEquals(c1, Pair.class);
}
  • 不適切なオーバーライドメソッド
class Pair<T> {
    public boolean equals(T t) {}
}

これは、定義されたequals(T t)メソッドが実際にはequals(Object t)に擦り替えられるためです。このメソッドはObjectから継承されたものであり、コンパイラは実際にオーバーライドされることになるジェネリックメソッドの定義を阻止します。

ジェネリクスの継承#

クラスはジェネリッククラスを継承できます。ジェネリック型を継承した場合、サブクラスは親クラスのジェネリック型を取得できます。例えば:IntPairは親クラスのジェネリック型Integerを取得できます。

public class IntPair extends Pair<Integer> {

    public IntPair(Integer first, Integer last) {
        super(first, last);
    }
}

class IntPairTest {
    @Test
    void getSuperParameterizedType() {
        Class<IntPair> clazz = IntPair.class;
        Type t = clazz.getGenericSuperclass();
        if (t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            Type[] types = pt.getActualTypeArguments(); // 多くの型があるかもしれません
            Type firstType = types[0];
            Class<?> typeClass = (Class<?>) firstType;
            assertEquals(typeClass, Integer.class);
        }
    }
}

extendsワイルドカード#

public class PairHelper {
    static int add(Pair<Number> p) {
        Number first = p.getFirst();
        Number last = p.getLast();
        return first.intValue() + last.intValue();
    }
}

image

上記のように、add関数が受け取る引数の型はPair<Number>であるため、Pair<Number>を渡すことには問題ありません。しかし、Pair<Integer>を渡すとコンパイルエラーが発生します。これは、IntegerNumberのサブクラスであるにもかかわらず、Pair<Integer>Pair<Number>のサブクラスではないためです。

この問題を解決するために、Pair<? extends Number>を使用することで、ジェネリック型がNumberのサブクラスであるPair型を受け取ることができます:

static int add(Pair<? extends Number> p) {
    Number first = p.getFirst();
    Number last = p.getLast();
    return first.intValue() + last.intValue();
}

@Test
void addNumberBased() {
    assertEquals(579, add(new Pair<Integer>(123, 456)));
}

読み取り#

  • List<? extends Number>からNumber型を取得できます。なぜなら、その要素はNumber型またはNumberのサブタイプだからです。
  • List<? extends Number>からInteger型を取得することはできません。なぜなら、その保存されている要素がDoubleである可能性があるからです。
  • List<? extends Number>からDouble型を取得することはできません。なぜなら、その保存されている要素がIntegerである可能性があるからです。

書き込み#

  • List<? extends Number>Numberを追加することはできません。なぜなら、その保存されている要素がIntegerまたはDoubleである可能性があるからです。
  • List<? extends Number>Integerを追加することはできません。なぜなら、その保存されている要素がDoubleである可能性があるからです。
  • List<? extends Number>Numberを追加することはできません。なぜなら、その保存されている要素がIntegerである可能性があるからです。
public class ExtendsTest {
    List<? extends Number> nums1 = new ArrayList<Number>();
    List<? extends Number> nums2 = new ArrayList<Integer>();
    List<? extends Number> nums3 = new ArrayList<Double>();

    void get() {
        Number x = nums1.get(0);
        // Integer y = nums2.get(0); // コンパイルエラー
        // Double z = nums3.get(0);  // コンパイルエラー
    }

    void set() {
        /*
        nums1.add(new Number() {
            @Override
            public int intValue() {
                return 0;
            }

            @Override
            public long longValue() {
                return 0;
            }

            @Override
            public float floatValue() {
                return 0;
            }

            @Override
            public double doubleValue() {
                return 0;
            }
        });
         */ // コンパイルエラー
        // nums2.add(new Integer(1));    // コンパイルエラー
        // nums3.add(new Double(1.2)); // コンパイルエラー
    }
}

superワイルドカード#

<? super T>はワイルドカードの下限を示し、具体的なジェネリックパラメータは次の条件を満たす必要があります:ジェネリックパラメータはT型またはその親クラスでなければなりません。

読み取り#

  • List<? super Integer>型のオブジェクトからInteger型のデータを読み取ることは保証されません。なぜなら、それがList<Number>型である可能性があるからです。
  • List<? super Integer>型のオブジェクトからNumber型のデータを読み取ることは保証されません。なぜなら、それがList<Object>型である可能性があるからです。
  • 唯一保証されるのは、List<? super Integer>型のオブジェクトからObjectオブジェクトのインスタンスを取得できることです。

書き込み#

上記の例のList<? super Integer>配列オブジェクトに対して:

  • Integerオブジェクトをarrayに追加できます。また、Integerのサブクラスのオブジェクトもarrayに追加できます。なぜなら、Integerおよびその親クラスはIntegerのサブクラスのオブジェクトを受け入れることができるからです。
  • Double/Number/Objectなど、Integerのサブクラスでないオブジェクトをarrayに追加することはできません。

image

extendssuper#

List<? super Integer> l1の場合:#

  • 正しい理解:? super Integerはジェネリックパラメータを制限します。l1のジェネリックパラメータをTとすると、TIntegerまたはIntegerの親クラスであるため、IntegerまたはIntegerのサブクラスのオブジェクトをl1に追加できます。
  • 誤った理解:? super Integerは挿入される要素の型を制限します。したがって、IntegerまたはIntegerの親クラスのオブジェクトはすべてl1に挿入できます。

List<? extends Integer> l2の場合:#

  • 正しい理解:? extends Integerはジェネリックパラメータを制限します。l2のジェネリックパラメータをTとすると、TIntegerまたはIntegerのサブクラスであるため、Xというクラスを見つけることはできません。XはジェネリックパラメータTのサブクラスであるため、要素をl2に追加することはできません。ただし、ジェネリックパラメータTIntegerまたはIntegerのサブクラスであることがわかっているため、l2から要素を読み取ることができ(取得した要素の型はIntegerまたはIntegerのサブクラス)、Integerに格納できます。
  • 誤った理解:? extends Integerは挿入される要素の型を制限します。したがって、IntegerまたはIntegerのサブクラスのオブジェクトはすべてl2に挿入できます。

応用:PECS#

PECS 原則:Producer Extends, Consumer Super

  • Producer extends: ListからT型のデータを提供する必要がある場合(つまり、ListからT型のデータを読み取ることを希望する場合)、? extends Tを使用する必要があります。例えば、List<? extends Integer>。ただし、このListにデータを追加することはできません。
  • Consumer Super: ListT型のデータを消費する必要がある場合(つまり、T型のデータをListに書き込むことを希望する場合)、? super Tを使用する必要があります。例えば、List<? super Integer>。ただし、このListから読み取るデータの型は保証されません。
  • 読み取りも書き込みも希望する場合は、ジェネリックパラメータの型を明示的に宣言する必要があります。例えば、List<Integer>
public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i = 0; i < src.size(); i++) {
            dest.set(i, src.get(i));
        }
    }
}

ソースコード#

参考文献#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。