Phase 3 · Core JavaModule 13~40 min read

Generics

Write type-safe, reusable code with generic classes, methods, bounded types, and wildcards.

What you'll learn

Generics let you write code that works with any type while staying completely type-safe. They're the reason List<String> knows it holds strings — and why you almost never need a cast in modern Java. This module demystifies those angle brackets.

By the end you'll be able to:

  • Explain why generics exist and the problems they solve
  • Write generic classes, methods, and interfaces
  • Constrain type parameters with bounded types
  • Use wildcards and apply the PECS principle
  • Understand type erasure and its limitations

Why generics exist

Before generics, collections held Object, so you cast every value on the way out and hoped you'd put the right thing in. Mistakes surfaced as ClassCastException at runtime. Generics move that safety to compile time — the compiler enforces the type for you:

The problem generics solve

Without generics

List list = new ArrayList();
list.add("hi");
String s = (String) list.get(0);  // manual cast
list.add(42);                     // oops, no error...
String bad = (String) list.get(1); // 💥 ClassCastException

Casts everywhere, and mistakes blow up at runtime.

With generics

List<String> list = new ArrayList<>();
list.add("hi");
String s = list.get(0);   // no cast!
list.add(42);             // ✗ compile error

Type-safe, cast-free, and errors caught at compile time.

Generic classes

A generic class takes a type parameter in angle brackets — conventionally <T> for "type," <E> for "element," <K, V> for "key, value." Inside the class, T stands in for whatever type the caller chooses when they create an instance:

Box.java
class Box<T> {                 // T is a type parameter
    private T item;
    void set(T item) { this.item = item; }
    T get() { return item; }
}

public class Main {
    public static void main(String[] args) {
        Box<String> b = new Box<>();
        b.set("Hello");
        String s = b.get();        // no cast needed
        System.out.println(s);
    }
}

Note

The <> on the right (new Box<>()) is the diamond operator — the compiler infers the type from the left side, so you don't repeat it.

Generic methods

A single method can be generic too, even in a non-generic class. Declare the type parameter before the return type. The compiler infers T from the arguments you pass, so one method works for every type:

GenericMethod.java
// <T> before the return type declares a generic method
static <T> T firstOf(T[] array) {
    return array[0];
}

public static void main(String[] args) {
    String[] names = {"Sara", "Omar"};
    Integer[] nums = {1, 2, 3};

    System.out.println(firstOf(names));  // Sara
    System.out.println(firstOf(nums));   // 1
}

Bounded type parameters

Sometimes T can't be anything — you need it to have certain capabilities. A bounded type parameter (<T extends Number>) restricts T to a type or its subtypes, letting you call that type's methods inside:

Bounded.java
import java.util.List;

// T must be a Number (or subclass), so we can call number methods
static <T extends Number> double sum(List<T> list) {
    double total = 0;
    for (T n : list) total += n.doubleValue();
    return total;
}

public static void main(String[] args) {
    System.out.println(sum(List.of(1, 2, 3)));     // 6.0
    System.out.println(sum(List.of(1.5, 2.5)));    // 4.0
}

Wildcards & the PECS principle

The wildcard ? represents an unknown type — useful in method parameters when you want flexibility. It comes in three forms:

Wildcard bounds

?

Unbounded

Any type. Read elements as Object. Use when the type doesn't matter.

? extends T

Upper bound

T or any subtype. A producer — you read T out of it.

? super T

Lower bound

T or any supertype. A consumer — you write T into it.

Wildcard.java
import java.util.List;

// accepts a List of ANY type
static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

public static void main(String[] args) {
    printAll(List.of(1, 2, 3));
    printAll(List.of("a", "b"));
}

PECS: Producer Extends, Consumer Super

A famous mnemonic: use ? extends T when a structure produces values you'll read, and ? super T when it consumes values you'll write. It tells you exactly which wildcard to reach for.

Type erasure

Generics are a compile-time feature. After the compiler checks your types, it erases them — at runtime a List<String> is just a List. This keeps generics compatible with old code, but it has consequences:

  • You can't write new T() or new T[] — the type isn't known at runtime
  • You can't use instanceof List<String> — only instanceof List
  • Two overloads that differ only by generic type (List<String> vs List<Integer>) clash

Note

Don't worry about memorising erasure's edge cases now — just know that generic type info exists for the compiler, not at runtime. It explains most "why can't I do that?" moments with generics.

Recap & quick check

Key takeaways

  • Generics provide compile-time type safety and remove the need for casts.
  • Generic classes and methods use a type parameter like <T> that the caller fills in.
  • Bounded types (<T extends Number>) let you require capabilities on T.
  • Wildcards: ? (any), ? extends T (producer/read), ? super T (consumer/write) — remember PECS.
  • Type erasure removes generic info at runtime, which is why new T() and generic instanceof aren't allowed.

Quick check

1. What is the main benefit of generics?

2. In List<String>, what is String?

3. What does <T extends Number> mean?

4. PECS stands for…

5. Because of type erasure, what is NOT allowed?

Nicely done — generics unlock type-safe, reusable code. Next up: Module 14 — the Java Collections Framework, the most-used generic library of all.