Wildcards

Generics Are Not Covariant

Integer is a Number, but List<Integer> is not a List<Number>. Wildcards (?) make methods flexible.

WildcardMeaningCan read asCan add
List<?>list of unknown typeObjectnothing (except null)
List<? extends Number>Number or subtype (producer)Numbernothing
List<? super Integer>Integer or supertype (consumer)ObjectInteger

Tip: Remember PECS: Producer Extends, Consumer Super. If you read from it, use extends; if you write into it, use super.

Wildcards in Action

Example

import java.util.ArrayList;
import java.util.List;

public class Main {
    static double total(List<? extends Number> prices) {      // producer: read only
        double sum = 0;
        for (Number n : prices) sum += n.doubleValue();
        return sum;
    }

    static void addDefaults(List<? super Integer> target) {   // consumer: write Integers
        target.add(10);
        target.add(20);
    }

    static void printAll(List<?> items) {
        System.out.println(items.size() + " items: " + items);
    }

    public static void main(String[] args) {
        List<Integer> ints = new ArrayList<>(List.of(1, 2));
        List<Double> doubles = List.of(1.5, 2.5);
        System.out.println(total(ints) + " " + total(doubles));

        List<Number> numbers = new ArrayList<>();
        List<Object> objects = new ArrayList<>();
        addDefaults(ints);
        addDefaults(numbers);
        addDefaults(objects);
        printAll(ints);
        printAll(numbers);
        printAll(List.of("x", 'y', 3));
    }
}

Output

3.0 4.0
4 items: [1, 2, 10, 20]
2 items: [10, 20]
3 items: [x, y, 3]

Type Erasure

Generics exist only at compile time. The compiler checks types, then erases them: at runtime a List<String> and a List<Integer> are the same ArrayList class. That is why you cannot write new T(), T[] creation, or obj instanceof List<String>.

Example

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> a = new ArrayList<>();
        List<Integer> b = new ArrayList<>();
        System.out.println(a.getClass() == b.getClass());
        System.out.println(a.getClass().getName());
    }
}

Output

true
java.util.ArrayList