Java Generics: Type Erasure, Wildcards and PECS
Codevity Admin · 10/25/2026
Step 14 of 38 · Java Interview Preparation for Beginners · Stage 3: Collections, generics and exceptions
Codevity Admin · 10/25/2026
Generics let a class or method work with many types while the compiler still checks them. List<String>, Map<String, Integer> and Optional<User> are all generic types. Interviewers ask about three ideas: type erasure, invariance, and wildcards (PECS).
Before Java 5, collections held Object, so you had to cast on every read, and a wrong cast failed only at run time with a ClassCastException. Generics move that check to compile time:
import java.util.*;
public class RawTypes {
@SuppressWarnings({"rawtypes", "unchecked"})
public static void main(String[] args) {
List raw = new ArrayList(); // raw type: avoid
raw.add("text");
try {
Integer wrong = (Integer) raw.get(0); // compiles, then fails at run time
} catch (ClassCastException e) {
System.out.println("ClassCastException at run time"); // ClassCastException at run time
}
List<String> safe = new ArrayList<>();
safe.add("text");
// safe.add(42); // compile error: caught immediately
}
}Never use raw types in new code: they switch off the compiler's checks.
import java.util.*;
public class GenericsDemo {
public static void main(String[] args) {
System.out.println(max(3, 7)); // 7
System.out.println(max("apple", "pear")); // pear
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sum(ints) + sum(doubles)); // 10.0
List<Number> sink = new ArrayList<>();
addIntegers(sink); // a List<? super Integer> accepts a List<Number>
System.out.println(sink); // [1, 2]
System.out.println(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()); // true (type erasure)
}
static <T extends Comparable<T>> T max(T a, T b) { // bounded type: T must be comparable
return a.compareTo(b) >= 0 ? a : b;
}
static double sum(List<? extends Number> numbers) { // producer: we only read from it
double total = 0;
for (Number n : numbers) total += n.doubleValue();
return total;
}
static void addIntegers(List<? super Integer> target) { // consumer: we only write to it
target.add(1);
target.add(2);
}
}<T extends Comparable<T>> is a bounded type parameter: it lets the method call compareTo on T.
Generics exist only at compile time. The compiler checks the types, inserts casts, and then erases the type arguments, so at run time List<String> and List<Integer> are both just List (the last line above prints true). This was done for backward compatibility with pre-generics code.
Consequences you can be asked about:
new T() or new T[10]: the type is unknown at run time.instanceof List<String>. Only instanceof List<?> is allowed.void f(List<String>) and void f(List<Integer>): after erasure they have the same signature.List<int> is illegal), so you use List<Integer> and pay for autoboxing.Arrays are covariant (String[] is an Object[]), but generics are invariant: List<String> is not a List<Object>, even though String is an Object. If it were allowed, you could add an Integer to what is really a list of strings.
import java.util.*;
public class Invariance {
public static void main(String[] args) {
List<String> strings = new ArrayList<>(List.of("a", "b"));
// List<Object> objects = strings; // compile error: generics are invariant
List<? extends Object> readOnly = strings; // allowed: a read-only view of "some kind of Object"
System.out.println(readOnly.get(0)); // a
Object[] array = new String[1]; // arrays ARE covariant...
try {
array[0] = 42; // ...so this compiles but fails at run time
} catch (ArrayStoreException e) {
System.out.println("ArrayStoreException"); // ArrayStoreException
}
}
}| Wildcard | Meaning | You can... |
|---|---|---|
List<?> | A list of some unknown type | Read items as Object. Add only null |
List<? extends Number> | A list of Number or a subtype | Read items as Number. You cannot add |
List<? super Integer> | A list of Integer or a supertype | Add Integers. Reads come back as Object |
PECS: Producer Extends, Consumer Super.
? extends T.? super T.That is why Collections.copy(List<? super T> dest, List<? extends T> src) has exactly that signature. In the example above, reads from the list, so it accepts a and a . writes to it, so it accepts a or .
sum(List<? extends Number>)List<Integer>List<Double>addIntegers(List<? super Integer>)List<Number>List<Object>List<?>List<Object>List<?>List<Object>List<Object>static <T> void swap(List<T> list, int i, int j)static void printAll(List<?> items)List<String>List<Integer>Collections.copy