Back to Advanced Topics
Q39Question 39 of 41

What is type erasure in generics, what does it make impossible, and what is the PECS rule?

Key Concepts
  • Type erasure removes generic type information after compile-time checking, replacing type parameters with their bounds or Object.
  • At runtime List<String> and List<Integer> are the same class (raw List), which is the single fact explaining every restriction.
  • new T(), instanceof List<String>, generic arrays like new T[], and generic-only overloads are impossible after erasure.
  • The standard workaround for new T() is passing a Class<T> token and constructing reflectively.
  • PECS means Producer Extends Consumer Super: use ? extends T to read and ? super T to write.
  • Collections.copy(List<? super T> dest, List<? extends T> src) is the canonical PECS example.
  • Erasure is why generics have zero runtime cost and why you sometimes see unchecked-cast warnings.

Definition

Type erasure deletes generic types at runtime so new T, instanceof on a parameterized type, generic arrays, and generic-only overloads are impossible, and PECS says read with extends and write with super.
  • Type erasure – The compiler's process of removing generic type information after compile-time checking, replacing type parameters with their bounds or Object, and inserting casts.
  • Runtime consequence – List and List are the same class at runtime (raw List), so the JVM has no knowledge of the element type.
  • Impossible operations – new T(), instanceof List, new T[], and overloading by generic type argument all fail because of erasure.
  • Class token workaround – Pass a Class to recover type information at runtime, e.g., clazz.getDeclaredConstructor().newInstance().
  • PECS – Producer Extends, Consumer Super: use ? extends T when reading from a structure and ? super T when writing into one.

Imagine National Bank of India stamps every form with a label like "this box only accepts savings-account records" while the clerk checks the forms (compile time). But once the forms are filed, the label is peeled off and every box just says "records" (runtime erasure). So later you cannot ask a filed box "were you originally the savings-only box?" because the label is gone. PECS is the handling rule: if you are reading records out of a box, label it "produces account records" so you can read safely; if you are putting records in, label it "accepts accounts or any parent type" so writes are valid.

Understanding the Concepts

What is type erasure?

At compile time the compiler fully checks generics. But after checking, it erases the type parameter: an unbounded T becomes Object, a bounded T extends Number becomes Number, and it inserts casts at call sites. The consequence is that List and List are the same class at runtime.

JAVA
package com.bank.service;

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

public class ErasureDemo {

    public void demonstrateErasure() {
        List<String> accounts = new ArrayList<>();
        List<Integer> numbers = new ArrayList<>();

        // At runtime, both are just ArrayList
        System.out.println(
            accounts.getClass() == numbers.getClass()); // true

        // The generic type is gone at runtime
        System.out.println(
            accounts.getClass().getName()); // java.util.ArrayList
    }
}

Why can you not write new T()?

At runtime T is erased to its bound, so the JVM does not know which constructor to call. The standard workaround is to pass a Class token and use reflection.

JAVA
package com.bank.repository;

import java.math.BigDecimal;

public class GenericRepository<T> {

    private final Class<T> entityClass;

    // Pass Class<T> token to recover type at runtime
    public GenericRepository(Class<T> entityClass) {
        this.entityClass = entityClass;
    }

    public T createNew() throws Exception {
        // new T() is impossible, so use reflection
        return entityClass.getDeclaredConstructor().newInstance();
    }
}
JAVA
// Usage
var repo = new GenericRepository<>(SavingsAccount.class);
SavingsAccount account = repo.createNew();

Why can you not write instanceof List?

The runtime only has raw List, with no memory of the element type. You can only test instanceof List (unbounded wildcard).

JAVA
package com.bank.service;

import java.util.List;

public class InstanceofDemo {

    public void checkType(Object obj) {
        // COMPILE ERROR: cannot check parameterized type
        // if (obj instanceof List<String>) { }

        // OK: unbounded wildcard
        if (obj instanceof List<?> list) {
            System.out.println("It is a List of size " + list.size());
        }
    }
}

Why can you not create generic arrays?

Arrays are reified: they carry their component type and check it at runtime with store checks. This directly conflicts with erased generics.

JAVA
package com.bank.service;

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

public class GenericArrayDemo {

    public void arrayProblem() {
        // COMPILE ERROR: cannot create generic array
        // List<String>[] accountLists = new List<String>[10];

        // Workaround: use List of Lists
        List<List<String>> accountLists = new ArrayList<>();

        // Or use Object[] with unchecked cast
        @SuppressWarnings("unchecked")
        List<String>[] arr = (List<String>[]) new List<?>[10];
    }
}

Why can you not overload by generic type?

After erasure, methods that differ only by generic type argument have the same raw signature and clash.

JAVA
package com.bank.service;

import java.util.List;
import java.math.BigDecimal;

public class OverloadClashDemo {

    // COMPILE ERROR: both erase to process(List)
    // public void process(List<String> accounts) { }
    // public void process(List<BigDecimal> amounts) { }

    // Fix: use different method names
    public void processAccounts(List<String> accounts) {
        System.out.println("Processing accounts");
    }

    public void processAmounts(List<BigDecimal> amounts) {
        System.out.println("Processing amounts");
    }
}

What is the PECS rule?

PECS stands for Producer Extends, Consumer Super. Use ? extends T when reading from a structure (producer) and ? super T when writing into it (consumer).

JAVA
package com.bank.service;

import java.util.List;
import java.util.ArrayList;
import java.math.BigDecimal;

public class PecsDemo {

    // PRODUCER: read accounts out (? extends Account)
    public BigDecimal sumBalances(
            List<? extends Account> accounts) {
        BigDecimal total = BigDecimal.ZERO;
        for (Account acc : accounts) {
            total = total.add(acc.getBalance());
        }
        return total;
        // Can read as Account
        // Cannot add to list (except null)
    }

    // CONSUMER: write accounts in (? super SavingsAccount)
    public void onboardNewAccounts(
            List<? super SavingsAccount> target) {
        target.add(new SavingsAccount("SAV2001",
                new BigDecimal("50000.00")));
        target.add(new SavingsAccount("SAV2002",
                new BigDecimal("75000.00")));
        // Can add SavingsAccount
        // When reading, only get Object back
    }
}

How does Collections.copy demonstrate PECS?

The canonical PECS example reads from an extends source and writes to a super destination.

JAVA
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;

// Signature: copy(List<? super T> dest, List<? extends T> src)
// src is PRODUCER (extends): we read from it
// dest is CONSUMER (super): we write into it

List<SavingsAccount> savings = List.of(
    new SavingsAccount("SAV2001", new BigDecimal("50000.00")));

List<Account> allAccounts = new ArrayList<>(savings);

// SavingsAccount extends Account:
// src: List<? extends Account> ← List<SavingsAccount> OK
// dest: List<? super Account> could accept List<Object>
Collections.copy(allAccounts, savings); // Compiles

How is ? extends different from ? super? (Comparison)

| Feature | ? extends T (Producer) | ? super T (Consumer) |
|---|---|---|
| Reading | Safe: every element is at least T | Unsafe: only get Object back |
| Writing | Cannot add (except null) | Safe: can add T values |
| Use when | Structure is a source you read from | Structure is a sink you write into |
| Example | sumBalances(List) | onboard(List) |
| Mnemonic | PE: Producer Extends | CS: Consumer Super |

What are common mistakes with erasure and PECS?

Getting the wildcard wrong is one of the most common sources of confusing "cannot be applied" compile errors.

JAVA
// WRONG: using extends when writing
public void addAccount(List<? extends Account> list) {
    // list.add(new SavingsAccount(...)); // COMPILE ERROR!
    // Cannot add to a producer
}

// CORRECT: use super when writing
public void addAccount(List<? super SavingsAccount> list) {
    list.add(new SavingsAccount("SAV2001",
            new BigDecimal("50000.00"))); // OK
}

// WRONG: thinking generics exist at runtime
if (list instanceof List<String>) { } // COMPILE ERROR

// CORRECT: use unbounded wildcard
if (list instanceof List<?>) { } // OK

What about unchecked cast warnings?

Erasure is why you see @SuppressWarnings("unchecked") on casts like (T) obj. The runtime cannot verify the cast, so the compiler warns you.

JAVA
package com.bank.service;

public class UncheckedCastDemo {

    @SuppressWarnings("unchecked")
    public <T> T safeCast(Object obj, Class<T> type) {
        if (type.isInstance(obj)) {
            return (T) obj; // Unchecked, but safe with isInstance guard
        }
        throw new ClassCastException(
            "Cannot cast to " + type.getName());
    }
}

What should a senior engineer know?

Erasure is why generics are a compile-time feature with zero runtime cost. It is also why you sometimes pass a Class token or a TypeReference to recover type information at runtime, as Jackson and Spring do. PECS is not academic: getting the wildcard wrong is a very common cause of "cannot be applied" compile errors in real APIs. If you both read and write, use an exact T with no wildcard.

Visual Overview

DIAGRAM
┌───────────────────────────────────────────────────┐
│              Type Erasure Process                   │
├───────────────────────────────────────────────────┤
│                                                   │
│  Source Code:                                     │
│    List<String> accounts = new ArrayList<>();     │
│    accounts.add("SAV2001");                        │
│    String id = accounts.get(0);                   │
│                                                   │
│  After Erasure (Bytecode):                        │
│    List accounts = new ArrayList();               │
│    accounts.add("SAV2001");                        │
│    String id = (String) accounts.get(0); // cast  │
│                                                   │
│  Runtime: List<String> == List<Integer> == List    │
└───────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────┐
│              PECS: Producer Extends Consumer Super │
├───────────────────────────────────────────────────┤
│                                                   │
│  ? extends T (PRODUCER)       ? super T (CONSUMER)│
│  ┌──────────────────┐        ┌──────────────────┐ │
│  │  Read: T ✓       │        │  Read: Object    │ │
│  │  Write: ✗        │        │  Write: T ✓      │ │
│  │                  │        │                  │ │
│  │  List<? extends  │        │  List<? super    │ │
│  │    Account>      │        │    SavingsAcct>  │ │
│  │  ← read balances │        │  → add accounts  │ │
│  └──────────────────┘        └──────────────────┘ │
└───────────────────────────────────────────────────┘

Key Takeaway

Type erasure is the compiler's process of using generic type information to check code at compile time and then removing it, replacing each type parameter with its bound or Object and inserting casts. The practical consequence is that at runtime a List and a List are the same class, plain List, because the element type is gone. So generics are a compile-time safety feature with zero runtime cost.

That one fact makes several things impossible. new T() fails because at runtime T is erased and the JVM does not know which constructor to call, so the workaround is passing a Class token. instanceof on a parameterized type like List fails because the runtime only sees raw List. Generic arrays like new T[] conflict with arrays' reified store checks. And overloading by generic type argument clashes because both methods have the same raw signature.

PECS (Producer Extends, Consumer Super) is the rule for wildcard bounds. A List is a producer: you can safely read every element as T but cannot add. A List is a consumer: you can safely add T values but when reading you only get Object back. If a parameter is a source you read from, use extends. If it is a sink you write into, use super. If you both read and write, use an exact T.

In banking work at National Bank of India a generic repository could not use new T(), so it took a Class token and created instances reflectively. A reporting utility summed balances over a List so it could accept a list of SavingsAccount and read every element as Account. An onboarding utility added savings records into a List so it could write into a list typed to Account.

Remember one sentence: type erasure removes generic types at runtime making new T() and instanceof on parameterized types impossible, and PECS says use extends to read and super to write.

---

Share
Use ← → arrow keys to navigate