Start Learning
Javaneer
Back to stage
Stage 3·Intermediate Java

Generics

Type parameters, bounded types, wildcards (PECS), and type erasure.

20 min readIntermediate
On this page

You've seen angle brackets everywhere: List<String>, Map<String, Integer>. That's generics - Java's way to write code that works with any type while staying completely type-safe. It's how collections avoid casting and catch mistakes at compile time.

The problem generics solve

Before generics, collections held raw Objects. You had to cast everything, and mistakes blew up at runtime:

List list = new ArrayList();     // old, raw style
list.add("hello");
String s = (String) list.get(0); // manual cast, ugly
list.add(42);                    // oops - compiles, but...
String bad = (String) list.get(1); // 💥 ClassCastException at runtime

With generics, you declare the type once and the compiler enforces it:

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);   // no cast needed
// list.add(42);          // ❌ won't even compile - caught early!
Generics are like a labeled container

An unlabeled box (raw type) could contain anything - you must open and check every time. A box labeled "CDs only" (List<CD>) guarantees its contents: the label-checker (compiler) refuses anything else at the door, so you never open it to find a surprise. Generics are that type label.

Writing your own generic class

Use a type parameter (conventionally T) as a placeholder for a real type:

class Box<T> {
    private T value;
    void put(T value) { this.value = value; }
    T get()           { return value; }
}
Box<String> sBox = new Box<>();
sBox.put("hi");
String s = sBox.get();     // typed as String, no cast

Box<Integer> iBox = new Box<>();
iBox.put(42);

One class, safely reusable for any type. Common conventional names: T (type), E (element), K/V (key/value).

Generic methods

Methods can be generic too:

static <T> T firstOf(List<T> items) {
    return items.get(0);
}

String name = firstOf(List.of("Ada", "Grace"));  // T inferred as String

Bounded types: constraining T

Sometimes T must be something specific. A bound with extends requires T to be a subtype:

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

Wildcards: flexible parameters (PECS)

? is a wildcard - "some unknown type". The rule of thumb is PECS: Producer extends, Consumer super.

// Reading FROM (producing): use ? extends
double total(List<? extends Number> nums) { ... }   // accepts List<Integer>, List<Double>...

// Writing INTO (consuming): use ? super
void addNumbers(List<? super Integer> list) {       // accepts List<Integer>, List<Number>...
    list.add(1);
    list.add(2);
}

Remember PECS

Producer Extends, Consumer Super. If a collection produces values you read out, use ? extends. If it consumes values you put in, use ? super. This maximizes flexibility while staying type-safe.

Type erasure

Generics are a compile-time feature. After compilation, the type information is largely erased - at runtime, List<String> and List<Integer> are both just List. This has consequences:

  • You can't do new T() or new T[] directly.
  • You can't check if (x instanceof List<String>).
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());  // true - both just ArrayList

Why erasure?

Erasure kept generics backward-compatible when they arrived in Java 5 - old code without generics still runs. The trade-off is that generic type info isn't available at runtime. In everyday code you'll rarely bump into this, but it's good to understand why some things aren't allowed.

Quick check

What is the main benefit of generics over using raw Object collections?

Key takeaways

  • Generics let you write type-safe, reusable code that works with any type.
  • A type parameter (T, E, K, V) is a placeholder for a real type supplied at use.
  • Bounded types (<T extends Number>) constrain what T can be, enabling method calls on it.
  • Wildcards use PECS: Producer extends (? extends), Consumer super (? super).
  • Generics are erased at runtime (type erasure), so List<String> and List<Integer> are both just List.

Next, we'll leave memory behind and work with the outside world: reading and writing files.