Generics and Polymorphism in Java: Subtyping Rules Explained

Illustration for Generics and Polymorphism in Java: Subtyping Rules Explained
By Last updated:

Polymorphism is at the heart of Java, enabling flexibility and reusability. Generics enhance polymorphism by allowing type-safe subtyping relationships, but they also introduce rules that may surprise developers.

For example:

List<String> strings = new ArrayList<>();
List<Object> objects = strings; // ❌ Compile error!

At first glance, this looks like it should work because String is a subtype of Object. However, Java generics are invariant, meaning List<String> is not a subtype of List<Object>.

This tutorial explores subtyping rules in generics, explaining extends, super, wildcards, PECS, and how to design flexible APIs safely.


Core Definition and Purpose of Java Generics

Generics were introduced in Java 5 to:

  1. Provide compile-time type safety.
  2. Eliminate the need for explicit casting.
  3. Enhance polymorphism with flexible yet type-safe APIs.

Type Parameters in Generics

  • <T> – General type.
  • <E> – Element (collections).
  • <K, V> – Key-Value pairs (maps).
class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Invariance of Generics

Unlike arrays, generics are invariant.

String[] strArray = new String[10];
Object[] objArray = strArray; // Allowed

List<String> strList = new ArrayList<>();
// List<Object> objList = strList; // Compile error!

Why? Because invariance prevents runtime type corruption.


Covariance with extends

Covariance allows read-only access to generics.

List<? extends Number> list = new ArrayList<Integer>();
Number num = list.get(0); // Allowed
// list.add(10); // Compile error

Use extends when the method produces data.


Contravariance with super

Contravariance allows write access.

List<? super Integer> list = new ArrayList<Number>();
list.add(10); // Allowed
Object obj = list.get(0); // Only Object can be retrieved

Use super when the method consumes data.


Wildcards in Polymorphism

  • ? – Unknown type.
  • ? extends T – Upper-bounded (read).
  • ? super T – Lower-bounded (write).
public static void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

PECS Principle (Producer Extends, Consumer Super)

  • Producer Extends – Use ? extends T when the collection produces values.
  • Consumer Super – Use ? super T when the collection consumes values.
public void copy(List<? extends Number> src, List<? super Number> dest) {
    for (Number n : src) {
        dest.add(n);
    }
}

Multiple Type Parameters and Nested Generics

Map<String, List<Integer>> map = new HashMap<>();

Polymorphism applies only with correct wildcards, not raw subtyping.


Generics and Collections Framework

  • List<? extends Number> accepts List<Integer>, List<Double>.
  • List<? super Integer> accepts List<Number>, List<Object>.
List<Integer> ints = Arrays.asList(1, 2, 3);
List<? extends Number> nums = ints; // Allowed

Raw Types vs Parameterized Types

List list = new ArrayList(); // Raw type
list.add("Hello");
list.add(123); // Unsafe

Parameterized types enforce polymorphism safely.


Type Inference and Diamond Operator

Map<String, Integer> map = new HashMap<>();

The compiler infers types, ensuring safe subtyping.


Type Erasure and Subtyping

At runtime, generics erase to raw types. Subtyping is enforced only at compile time.

System.out.println(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()); // true

Reifiable vs Non-Reifiable Types in Polymorphism

  • List<?> → Reifiable.
  • List<String> → Non-reifiable (erased to List).

Recursive Type Bounds

public static <T extends Comparable<T>> T max(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElse(null);
}

Compile-time subtyping ensures type safety.


Case Studies

Type-Safe Cache

class Cache<K, V> {
    private Map<K, V> store = new HashMap<>();
    public void put(K key, V value) { store.put(key, value); }
    public V get(K key) { return store.get(key); }
}

Repository Pattern

interface Repository<T, ID> {
    T findById(ID id);
}

Polymorphism via type parameters avoids unsafe casts.


Best Practices for Generics and Polymorphism

  • Prefer wildcards over raw types.
  • Use PECS principle.
  • Keep APIs simple – avoid deeply nested generics.
  • Don’t force covariance/contravariance without clear need.

Common Anti-Patterns

  • Using raw types in collections.
  • Forcing List<Object> to accept List<String>.
  • Overusing wildcards (List<? super ? extends T>).

Performance Considerations

Generics and polymorphism have no runtime cost due to erasure. Safety is enforced at compile time.


📌 What's New in Java for Generics?

  • Java 5: Generics + type-safe collections.
  • Java 7: Diamond operator simplifies polymorphism.
  • Java 8: Streams/APIs rely heavily on covariance.
  • Java 10: var works seamlessly with generics.
  • Java 17+: Sealed classes integrate with generic hierarchies.
  • Java 21: Virtual threads with generic concurrent APIs.

Conclusion and Key Takeaways

Generics bring polymorphism with safety, but subtyping rules can be tricky:

  • Generics are invariant by default.
  • Use extends for producers, super for consumers (PECS).
  • Wildcards provide flexibility without losing safety.
  • Runtime uses erasure, so rules apply at compile time only.

FAQ on Generics and Polymorphism

Q1: Why isn’t List<String> a subtype of List<Object>?
Because generics are invariant.

Q2: When should I use ? extends T?
When reading/producing values.

Q3: When should I use ? super T?
When writing/consuming values.

Q4: What is the PECS principle?
Producer Extends, Consumer Super.

Q5: Do wildcards exist at runtime?
No, they are erased but checked at compile time.

Q6: Can I assign List<Integer> to List<? extends Number>?
Yes, covariance allows it.

Q7: Can I assign List<Integer> to List<Number>?
No, invariance prevents it.

Q8: Do generics affect runtime performance?
No, they’re erased at compile time.

Q9: Can I use generics with arrays?
Not safely; arrays are covariant, generics are invariant.

Q10: How do frameworks like Spring use polymorphism with generics?
They use wildcards and reflection to adapt generic repositories safely.