SwadhyAI Notes

All Java notes

Java Generics

›Short versionOpen if you only have a minute.
  • A type parameter such as T is checked when you compile. Erasure removes it, and the JVM sees the bound, or Object.
  • List<Integer> is not a List<Number>. Generic types are invariant so a write cannot smuggle the wrong subtype in.
  • ? extends T is a producer: you can read T, and you cannot add a T. ? super T is a consumer: you can add a T, and a read is Object.
  • Heap pollution is a parameterized variable pointing at the wrong contents. Raw types and unchecked casts are how it gets in. The crash is a ClassCastException later.
  • new T(), instanceof List<String>, and overloads that erase to the same signature are illegal because T is gone at runtime.
  • When a method truly needs the class at runtime, pass a Class<T> token. The type parameter alone will not still be there.

A lets one class carry a String today and an Integer tomorrow, with the compiler refusing to mix them. The promise is real at compile time. is the decision Java made so that promise could ship on a JVM that already had ArrayList of Object. The parameter is checked, then deleted.

A parameter the compiler can see

java
class Box<T> {
    private T content;

    public void set(T content) {
        this.content = content;
    }

    public T get() {
        return content;
    }
}

Box<String> words = new Box<>();
words.set("Hello");
String word = words.get();

Box<Integer> numbers = new Box<>();
numbers.set(123);
// numbers.set("text"); // illegal: T is Integer here

T is a name for the caller’s choice. A generic method can make the same choice for one call, on a class that is otherwise ordinary.

java
public static <T> void printAll(T[] items) {
    for (T item : items) {
        System.out.print(item + " ");
    }
    System.out.println();
}

A is how you ask for methods. Inside Calculator<T extends Number>, num.doubleValue() is safe, and Calculator<String> is rejected. Names are a convention: E for an element, K and V for a map, T for a type. The compiler cares about the bound, not the letter.

Why the list of integers is not a list of numbers

feels strict until you picture the write. List<Integer> passed as a List<Number> would let the method add a Double. The caller’s next Integer read would be a lie. So the substitution is refused, even though Integer extends Number.

A describes the use instead of pretending you know the element. Read from a producer. Write to a consumer. The mnemonic is producer-extends, consumer-super, and it is exactly the safety argument above.

java
static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number number : numbers) {
        total += number.doubleValue();
    }
    // numbers.add(Integer.valueOf(1)); // illegal: the list might be List<Double>
    return total;
}

static void addZeros(List<? super Integer> values) {
    values.add(0);                 // Integer fits Integer, Number, and Object
    Object first = values.get(0);  // the read type is only Object
}

List<Integer> scores = List.of(1, 2, 3);
double total = sum(scores);

? extends Number might be a list of integers or a list of doubles. Reading each element as Number is honest. Adding an integer is honest for only one of those lists, so it is rejected. ? super Integer might be a list of integers, numbers, or objects. An integer belongs in all three. An element you pull back out is only known to be an Object.

Erasure, and the pollution it cannot stop

names.getClass() == scores.getClass() when one list was declared List<String> and the other List<Integer>. Both objects are ArrayList. The element type lived in the compiler, which inserted a cast at each get. You cannot test instanceof List<String>, you cannot write new T(), and you cannot overload print(List<String>) against print(List<Integer>). After erasure those are one method.

The hole is a raw type, or an unchecked cast. The variable still says List<String>. The object has been given something else. That disagreement is . The compiler’s unchecked warning is the last moment it can tell you. The exception arrives later, on the cast it inserted in good faith.

java
List<String> names = new ArrayList<>();
List raw = names;          // raw type: the compiler warns
raw.add(Integer.valueOf(1));

String first = names.get(0); // ClassCastException
// the compiler inserted (String) names.get(0)

A method takes List<Number> and you try to pass a List<Integer>. The compiler refuses, even though every integer is a number. What write becomes possible if that call were allowed?

The method could execute numbers.add(Double.valueOf(1.5)). The caller’s list was built as List<Integer>, and it would now contain a double. The next read that expects an integer fails with ClassCastException. Invariance blocks that call. A method that only reads can take List<? extends Number> and accept the integer list, because that signature has no legal add of a number.