Skip to content

Generics — Type Parameters, Wildcards, Bounded Types, Type Erasure, and PECS

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Generics. We cover key concepts, practical examples, and best practices to help you master this topic.

Java generics enable type-safe programming by parameterizing types, eliminating casts and enabling compile-time Type Checking. Before generics (Java 5), collections held Object references — every retrieval required a cast, and incorrect casts threw ClassCastException at runtime.

What You'll Learn

  • Generic classes, methods, and interfaces
  • Type parameter naming conventions
  • Wildcards: ? extends T and ? super T
  • Type erasure and its implications
  • The PECS principle (Producer Extends, Consumer Super)

Why It Matters

Generics are the backbone of the Collections Framework. Without understanding bounded wildcards, you will struggle to write flexible, reusable code. Type erasure explains weird limitations like "no generic arrays" and "cannot use instanceof with parameterized types."

Real-World Use

List<String>, Map<String, User>, Comparator<Employee> — every collection uses generics. Spring's RestTemplate, JPA's TypedQuery, and every functional interface (Predicate<T>) rely on generics.


Generic Classes

public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

Box<String> stringBox = new Box<>("Hello");
String value = stringBox.getValue(); // no cast needed

Multiple Type Parameters

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
}

Pair<Integer, String> pair = new Pair<>(1, "one");

Generic Methods

public class Utils {
    public static <T> T firstElement(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
}

String first = Utils.firstElement(List.of("a", "b"));
Integer max = Utils.max(5, 10);

The type parameter <T> is declared before the return type.

Bounded Type Parameters

Upper Bound (extends)

public static <T extends Number> double sum(T a, T b) {
    return a.doubleValue() + b.doubleValue();
}

sum(5, 10);       // int -> Integer, OK
sum(3.14, 2.5);   // double -> Double, OK
sum("a", "b");     // COMPILE ERROR: String does not extend Number

Multiple Bounds

public static <T extends Comparable<T> & Serializable> void process(T item) {
    // T must implement both Comparable and Serializable
}

Wildcards

Unbounded Wildcard (?)

public static void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

Upper Bounded Wildcard (? extends T)

public static double sumOfList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) {
        sum += n.doubleValue();
    }
    return sum;
}

List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
sumOfList(ints);    // OK
sumOfList(doubles); // OK

Lower Bounded Wildcard (? super T)

public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK
addNumbers(objects); // OK
List<Integer> ints = new ArrayList<>();
addNumbers(ints);    // OK

The PECS Principle

PECS stands for Producer Extends, Consumer Super.

  • Use ? extends T when you produce (read) items of type T
  • Use ? super T when you consume (write) items of type T
// Producer — read only
void copy(List<? extends T> source, List<? super T> dest) {
    for (T item : source) {  // safe to read
        dest.add(item);       // safe to write
    }
}

Why Not Just Use List<T>?

void copy(List<T> source, List<T> dest) { }

List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
copy(ints, nums); // COMPILE ERROR: List<Integer> != List<Number>

With wildcards:

void copy(List<? extends T> source, List<? super T> dest) { }
copy(ints, nums); // OK: Integer extends Number, Number super Number

Type Erasure

Generics are a compile-time feature. The compiler erases type parameters and inserts casts:

// Source
Box<String> box = new Box<>("hello");
String s = box.getValue();

// After erasure (roughly)
Box box = new Box("hello");
String s = (String) box.getValue();

Consequences of Erasure

  1. Cannot create arrays of parameterized types:

    List<String>[] = new List<String>[10]; // COMPILE ERROR
    List<String>[] = new List[10];         // OK (unchecked warning)
    
  2. Cannot use instanceof with parameterized types:

    if (obj instanceof List<String>) { } // COMPILE ERROR
    if (obj instanceof List) { }         // OK (unchecked warning)
    
  3. Cannot use primitive types as type parameters:

    List<int> // COMPILE ERROR — use List<Integer>
    
  4. Cannot overload methods with same erasure:

    void process(List<String> list) { }
    void process(List<Integer> list) { } // COMPILE ERROR: same erasure
    

Common Mistakes

  1. Using raw types. List list = new ArrayList(); bypasses generic type checking. Always use parameterized types.
  2. Mixing generic arrays. T[] array = new T[10]; does not compile due to erasure. Use ArrayList<T> instead.
  3. Forgetting wildcards in method parameters. void Process(List<Number> list) cannot accept List<Integer>.
  4. Using ? extends T for both reading and writing. You cannot add to a List<? extends Number> (except null) because the exact type is unknown.
  5. Assuming List<Object> is the supertype of all List<T>. It is not. List<String> is not a subtype of List<Object>.

Practice Questions

1. What is type erasure?
The removal of generic type information at compile time. The compiler replaces type parameters with their bounds or Object, and inserts necessary casts.

2. What is the difference between List<T> and List<?>?
List<T> is a specific parameterized type. List<?> is a list of an unknown type — you can read from it (as Object) but cannot add to it (except null).

3. What does the PECS principle state?
Producer Extends (use ? extends T when reading), Consumer Super (use ? super T when writing).

4. Can you create an array of List<String>?
No. The compiler prevents it because the array would not be type-safe after erasure. Use ArrayList<List<String>> instead.

5. Why does List<Integer> not extend List<Object>?
Because generics are invariant. If List<Integer> were a subtype of List<Object>, you could add a String to what is supposed to be a list of integers.

Challenge Question:
Implement a type-safe copy() method that copies from a source list to a destination list using PECS. Then implement a fill() method that fills a list with values from a supplier. Also implement a flatten() method that takes a List<List<? extends T>> and returns a List<T>.

FAQ

What is a raw type and why is it bad?

A raw type is a generic type used without type parameters (e.g., List instead of List<String>). It bypasses compile-time type checking, potentially causing ClassCastException at runtime. The compiler generates unchecked warnings for raw types.

Can I use generics with primitive types?

No. Type parameters must be reference types. Use wrapper classes (Integer, Double) instead. Java's autoboxing handles the conversion automatically.

What is the diamond operator?

The diamond operator (<>) allows the compiler to infer type parameters from context: Map<String, List<Integer>> map = new HashMap<>();. It was introduced in Java 7.

Can I have a generic exception class?

No. The Throwable class and its subclasses cannot be generic. The JVM does not support parameterized exception types.

{{< faq "What is @SuppressWarnings(\"unchecked\")?" "An annotation that suppresses unchecked warnings, typically necessary when writing generic code that the compiler cannot fully verify. Use it judiciously and document why the cast is safe." >}}

Mini Project

Write a program GenericsDemo.java that:

  1. Defines a generic Repository<T, ID> interface with methods save(T entity), findById(ID id), findAll(), delete(T entity)
  2. Implements it with an InMemoryRepository<T, ID> using a Map<ID, T>
  3. Uses bounded type parameters to ensure ID is Serializable
  4. Creates a UserRepository extends InMemoryRepository<User, Long>
  5. Demonstrates PECS with a method void transfer(List<? extends T> source, List<? super T> dest)
  6. Shows raw type warning and how to fix it
  7. Demonstrates that List<Integer> is not List<Object> with a failed compilation example

What's Next

Generics make collections type-safe, but equals() and hashCode() make them work correctly. Lesson 28 covers the equals and hashCode contract — why you must override both, how to implement them correctly, and how Lombok and records handle them automatically.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro