Generics - Wildcards & Type Erasure

Java generics use type erasure: the compiler performs type checking, but at runtime the type parameters are erased to their bounds (or Object if unbounded). Wildcards?, ? extends T, and ? super T—express “read-only / write-only” or “some subtype / supertype” constraints, making it safer to read and write collections. This article explains erasure, wildcard semantics, and PECS with examples and tables.

Overview

  • Type erasure: Generic information is removed after compilation. List<String> at runtime is just List. You cannot new T(), use T.class, or have List<String>.class (only List.class). Bridge methods preserve polymorphism.
  • ? extends T (upper bound): “Some subtype of T”. Safe to read as T; cannot write (except null). Use for producers or read-only parameters.
  • ? super T (lower bound): “Some supertype of T”. Safe to write T and its subtypes; can only read as Object. Use for consumers or write-only parameters.
  • PECS: Producer extends, Consumer super. Producers use extends; consumers use super.

Example

Example 1: Erasure — no generics at runtime

Java
List<String> list = new ArrayList<>();
// After compilation: List list = new ArrayList();
// list.add("a") becomes list.add((Object)"a"); elements are cast to String on get
  • You cannot write if (obj instanceof List<String>) or new T(). For runtime types, pass Class<T> or explicit type parameters.

Example 2: extends — read-only

Java
void print(List<? extends Number> list) {
    for (Number n : list) { System.out.println(n); }
    // list.add(1);  // Compile error: ? might be Integer or Double, cannot write
}
print(Arrays.asList(1, 2));
print(Arrays.asList(1.0, 2.0));
  • Accepts List<Integer>, List<Double>, etc., and reads safely as Number; you cannot add (except null).

Example 3: super — write-only

Java
void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    // Integer x = list.get(0);  // Compile error: get returns Object
}
List<Number> nums = new ArrayList<>();
addNumbers(nums);
  • You can write Integer into List<Number>, List<Object>, etc.; reads return Object only.

Example 4: PECS in Collections.copy

Java
// From java.util.Collections
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}
  • src is a producer (you read from it) → extends T. dest is a consumer (you write to it) → super T.

Example 5: Comparison and PECS

WildcardCan read asCan writeTypical use
? extends TT and subtypesNo (only null)Producer, read-only parameter
? super TObject onlyT and subtypesConsumer, write-only parameter
?ObjectOnly nullRarely; when type is fully unknown
  • PECS: If the parameter produces elements (you read from it) → use extends. If it consumes elements (you write to it) → use super.

Example 6: Raw types and unchecked casts

Java
List raw = new ArrayList();  // Raw type, no generics
raw.add("hello");
List<String> strList = raw;  // Unchecked; may fail at runtime
  • Avoid raw types; use generics or wildcards so the compiler catches errors.

Core Mechanism / Behavior

  • Erasure: Generic types are replaced by their bounds at compile time (e.g. <T extends Comparable> → Comparable); unbounded → Object. Casts and checks happen at compile time.
  • Bridge methods: When a subclass overrides a generic method, the compiler generates synthetic methods so polymorphism works with erased signatures.
  • Wildcards: The compiler allows only calls that are safe for the given bound; it cares about “what can be read” and “what can be written,” not the exact type of ?.
  • Reification: Java generics are not reified; you cannot inspect type parameters at runtime. Use Class<T> or type tokens when you need runtime type information.

Type Tokens and Runtime Types

When you need the concrete type at runtime (e.g. for deserialization), pass Class<T>:

Java
public <T> T fromJson(String json, Class<T> type) {
    return objectMapper.readValue(json, type);
}
User u = fromJson(str, User.class);
  • Super Type Token: For List<String>, use TypeReference (Jackson) or a similar pattern, since erasure removes List<String> at runtime.

Key Rules

  • For “read-only / producer” use ? extends T; for “write-only / consumer” use ? super T; PECS helps choose.
  • When you need the concrete type at runtime, pass Class<T> or a type token; do not rely on T after erasure.
  • Avoid raw types and unchecked casts; express constraints with generics or wildcards so the compiler finds bugs.
  • Prefer bounded type parameters (<T extends Comparable<T>>) when you need to call methods on T inside the generic code.

What's Next

See Reflection & Annotations for Class<T> and generics. See Collections for generics and wildcards in the standard library.