0xe3aad

0xe3aad

Java Generics

Type Erasure#

The Compiler Treats Type <T> as 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;
    }
}

Generics in Java are handled at compile time. The compiler applies type erasure to the code above, and the code seen by the JVM is as follows:

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;
    }
}

The Compiler Implements Safe Casting Based on <T>#

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

The compiler safely casts based on the type of T:

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

Limitations of Type Erasure#

  • <T> cannot be a primitive type because type erasure replaces it with Object.
  • It is impossible to obtain the Class carrying the generic type because generics are handled at compile time, so what you get is Pair<Object>.
    For example:
@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);
}
  • Improperly overridden methods
class Pair<T> {
    public boolean equals(T t) {}
}

This is because the defined equals(T t) method will actually be erased to equals(Object t), and this method is inherited from Object, so the compiler will prevent the definition of a generic method that would actually become an override.

Generic Inheritance#

A class can inherit a generic class. In the case of inheriting a generic type, the subclass can access the generic type of the parent class. For example, IntPair can access the generic type Integer of the parent class.

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(); // may have many types
            Type firstType = types[0];
            Class<?> typeClass = (Class<?>) firstType;
            assertEquals(typeClass, Integer.class);
        }
    }
}

extends Wildcard#

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

image

As shown, the add function accepts a parameter type of Pair<Number>, so passing Pair<Number> is fine; however, passing Pair<Integer> will result in a compilation error because although Integer is a subclass of Number, Pair<Integer> is not a subclass of Pair<Number>.

To solve this problem, you can use Pair<? extends Number>, which allows you to accept Pair types with generic types that are subclasses of Number:

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)));
}

Reading#

  • You can retrieve Number type from List<? extends Number> because its elements are of type Number or its subclasses.
  • You cannot retrieve Integer type from List<? extends Number> because its stored elements could be Double.
  • You cannot retrieve Double type from List<? extends Number> because its stored elements could be Integer.

Writing#

  • You cannot add Number to List<? extends Number> because its stored elements could be Integer or Double.
  • You cannot add Integer to List<? extends Number> because its stored elements could be Double.
  • You cannot add Number to List<? extends Number> because its stored elements could be 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); // compile error
        // Double z = nums3.get(0);  // compile error
    }

    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;
            }
        });
         */ // compile error
        // nums2.add(new Integer(1));    // compile error
        // nums3.add(new Double(1.2)); // compile error
    }
}

super Wildcard#

<? super T> describes the lower bound of the wildcard, meaning the specific generic parameter must satisfy the condition: the generic parameter must be of type T or its superclass.

Reading#

  • We cannot guarantee that we can read Integer type data from a List<? super Integer> type object because it might be of type List<Number>.
  • We cannot guarantee that we can read Number type data from a List<? super Integer> type object because it might be of type List<Object>.
  • The only guarantee is that we can retrieve an instance of Object from a List<? super Integer> type object.

Writing#

For the List<? super Integer> array object in the above example:

  • We can add Integer objects to array, and we can also add objects of subclasses of Integer to array, because Integer and its superclasses can accept subclasses of Integer.
  • We cannot add objects that are not subclasses of Integer, such as Double/Number/Object, to array.

image

extends vs super#

For List<? super Integer> l1:#

  • Correct understanding: ? super Integer limits the generic parameter. Let l1's generic parameter be T, then T is Integer or a superclass of Integer, so objects of Integer or subclasses of Integer can be added to l1.
  • Incorrect understanding: ? super Integer limits the type of elements inserted, so any object that is Integer or a superclass of Integer can be inserted into l1.

For List<? extends Integer> l2:#

  • Correct understanding: ? extends Integer limits the generic parameter. Let l2's generic parameter be T, then T is Integer or a subclass of Integer, thus we cannot find a class X such that X is a subclass of the generic parameter T, so we cannot add elements to l2. However, since we know that the generic parameter T is Integer or a subclass of Integer, we can read elements from l2 (the retrieved element type is Integer or a subclass of Integer) and store them in Integer.
  • Incorrect understanding: ? extends Integer limits the type of elements inserted, so any object that is Integer or a subclass of Integer can be inserted into l2.

Application: PECS#

PECS Principle: Producer Extends, Consumer Super

  • Producer extends: If we need a List to provide data of type T (i.e., we want to read data of type T from the List), then we need to use ? extends T, for example, List<? extends Integer>. However, we cannot add data to this List.
  • Consumer Super: If we need a List to consume data of type T (i.e., we want to write data of type T into the List), then we need to use ? super T, for example, List<? super Integer>. However, this List cannot guarantee the type of data read from it.
  • If we want to read and write, we must explicitly declare the type of the generic parameter, for example, 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));
        }
    }
}

Source code#

References#

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.