JAVA Contents

Generics, Wildcards, Invariance

Understand invariance, wildcards, and PECS to design type-safe and flexible APIs without unsafe casts or raw types leaking into production code.

On this page

Generics exist to move errors from runtime to compile-time

Before generics, Java collections stored Object. Type errors appeared at runtime as ClassCastException. Generics push these errors to compile-time. But to use them correctly, you must understand invariance and wildcards.

Invariance: the most misunderstood rule

In Java, generics are invariant.

List<Integer> is NOT a subtype of List<Number>

This surprises many developers. The reason is type safety.

Why invariance exists

If List<Integer> were a subtype of List<Number>, the following would be legal:

List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // imagine this was allowed
nums.add(3.14); // Double added into List<Integer>

This would break type safety. Therefore, generics are invariant.

Covariance with wildcards

If you want to accept a list of Integer, Double, or any subtype of Number, use an upper-bounded wildcard.

void printNumbers(List<? extends Number> numbers) {
  for (Number n : numbers) {
    System.out.println(n);
  }
}

This is covariance: the list produces Numbers safely.

Important restriction

With List<? extends Number>, you cannot add elements (except null):

numbers.add(10); // compile error

The compiler does not know the exact subtype.

Contravariance with lower bounds

If your method consumes values (adds them into a structure), use lower-bounded wildcards.

void addIntegers(List<? super Integer> list) {
  list.add(10);
}

This allows passing List<Integer>, List<Number>, or List<Object>.

PECS rule

Producer Extends, Consumer Super.

  • If a parameter produces T → use ? extends T
  • If a parameter consumes T → use ? super T

Example combining both

public static <T> void copy(
  List<? extends T> source,
  List<? super T> destination
) {
  for (T item : source) {
    destination.add(item);
  }
}

This is how Collections.copy works conceptually.

Raw types: never use them

List list = new ArrayList(); // raw type

Raw types disable generics safety and reintroduce runtime failures. In production code, raw types are almost always a smell.

Generic methods vs generic classes

Use generic methods when the type parameter is only relevant to one method.

public static <T> T first(List<T> list) {
  return list.get(0);
}

Use generic classes when the type parameter defines the entire object behavior.

Type erasure: runtime limitation

Java generics use type erasure. At runtime, List<String> and List<Integer> are both List.

  • You cannot do instanceof List<String>.
  • You cannot create new T() directly.

Production implication

Serialization frameworks, reflection-based libraries, and JSON mappers may require explicit type tokens to preserve type information.

Example: type token pattern

TypeReference<List<User>> typeRef = new TypeReference<>() {};

This captures generic type information for runtime frameworks.

API design and generics

Well-designed APIs use bounded generics to maximize flexibility while preserving type safety.

Example: flexible read-only API

public void processUsers(List<? extends User> users) {}

This allows List<AdminUser>, List<CustomerUser>, etc.

Avoid overcomplicated generic hierarchies

Complex generic bounds can reduce readability dramatically.

<T extends Comparable<T> & Serializable>

Use advanced bounds only when necessary.

Wildcard capture problem

Sometimes the compiler cannot infer type from wildcard. Introduce helper methods with explicit type parameters to resolve capture.

Common production pitfalls

  • Using raw types in legacy interop.
  • Overusing wildcards when a concrete type parameter is clearer.
  • Forgetting PECS and blocking flexibility.
  • Exposing overly strict generic signatures in public APIs.

Checklist

  • Remember: generics are invariant.
  • Use ? extends for producers.
  • Use ? super for consumers.
  • Avoid raw types.
  • Keep generic signatures readable.
  • Understand type erasure limitations.

Final principle

Generics are about designing safe and flexible contracts. If you misuse them, you either lose flexibility or lose safety. Mastering invariance is key to clean API design.