← Back to the section

Generics are a way to tell the compiler what type of data your class or method works with, before the program even runs. Thanks to this, errors are caught at compile time instead of blowing up at runtime, and manual casts disappear from your code.

source code compiler runtime List<String> listlist.add("hello")list.add(42)String s = list.get(0) <String> what may go in "hello" — ok 42 — error ArrayList <String> "hello"elements are Objectno brackets left get() returned String: the compiler added the cast

Angle brackets live until compilation ends: by them the compiler rejects list.add(42), and it inserts the cast on reading itself. The bytecode keeps a plain ArrayList, and the elements in it are Object.

Why generics: the problem without them

Imagine a list without generics — each element is stored as Object. You can put anything in, but when reading you must cast the type by hand, and the compiler gives you no help:

live example

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

public class RawList {
    public static void main(String[] args) {
        List list = new ArrayList();   // a "raw" list, no type
        list.add("hello");
        list.add(42);                  // the compiler stays silent — but it shouldn't

        try {
            String s = (String) list.get(1);  // we expect a string, there is a number
            System.out.println(s);
        } catch (ClassCastException e) {
            System.out.println("failed: " + e.getMessage());
        }
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

There are two problems here. First, a number accidentally ended up in the list and nobody noticed. Second, when reading you have to write (String) — a cast that is easy to get wrong.

Generics remove both problems. Specify the type in angle brackets, and the compiler starts watching over you:

live example

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

public class TypedList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(); // strings only
        list.add("hello");
        // list.add(42);                       // compile error — good!

        String s = list.get(0);                // no cast needed
        System.out.println(s.toUpperCase());   // HELLO
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Short formula: generics move type checking from runtime to compile time and get rid of manual casts.

Generic classes

You can make your own class generic too. The type parameter is written in angle brackets after the class name — by convention a single capital letter: T (type), E (element), K/V (key/value). When using it, we substitute a concrete type, and T inside the class effectively becomes it:

live example

public class BoxDemo {
    public static void main(String[] args) {
        Box<String> textBox = new Box<>();
        textBox.set("data");
        String text = textBox.get();               // already String, no cast
        System.out.println(text);

        Box<Integer> numberBox = new Box<>();
        numberBox.set(100);
        System.out.println(numberBox.get() + 1);   // 101 — numbers, not strings
    }
}

// a container that holds a single value of any type
class Box<T> {
    private T value;
    void set(T value) { this.value = value; }
    T get() { return value; }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Sometimes you need to restrict which types are allowed. The notation <T extends Number> means "T is a Number or any of its subclasses". Inside the class you then have access to Number's methods:

class Calculator<T extends Number> {
    private final T value;

    Calculator(T value) { this.value = value; }

    double half() {
        return value.doubleValue() / 2; // doubleValue() is available on Number
    }
}

Generic methods

A method can declare its own type parameter, even if the class itself is not generic. The parameter is written before the return type, and at the call site it is most often inferred automatically from the arguments, so you don't need to state it explicitly:

live example

import java.util.List;

public class FirstOrNull {
    public static void main(String[] args) {
        List<String> names = List.of("Anna", "Boris");
        String first = firstOrNull(names);        // T is inferred as String
        System.out.println(first);

        List<String> empty = List.of();
        System.out.println(firstOrNull(empty));   // empty list — null
    }

    // <T> declares a type parameter for this method only
    static <T> T firstOrNull(List<T> items) {
        return items.isEmpty() ? null : items.get(0);
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Wildcards: ? extends and ? super

Often a method needs to accept a list of "something" without being tied to one specific type. For that there is the question mark ? — the wildcard. It has two useful forms.

? extends Type — "this type or its subclass". It is convenient to read from such a collection: we know for sure that every element is at least a Number.

live example

import java.util.List;

public class Pecs {
    public static void main(String[] args) {
        System.out.println(sum(List.of(1, 2, 3)));    // List<Integer> — 6.0
        System.out.println(sum(List.of(0.5, 1.5)));   // List<Double> — 2.0
    }

    // accepts List<Integer>, List<Double>, List<Number> — any of them
    static double sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number n : numbers) {   // reading as Number — safe
            total += n.doubleValue();
        }
        return total;
        // numbers.add(anything) is forbidden here: the exact type is unknown
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

? super Type — "this type or its ancestor". It is convenient to write into such a collection: we can safely put in an Integer (or its subclass), because the target holds at least an Integer.

// you can pass List<Integer>, List<Number>, List<Object>
static void addNumbers(List<? super Integer> target) {
    target.add(1);  // adding an Integer is safe
    target.add(2);
}

To remember which one to use when, there's the rule PECS — Producer Extends, Consumer Super: if the collection gives you data (producer) — use extends; if you put data into it (consumer) — use super.

Type erasure

The main feature of generics in Java: they exist only at compile time. After type checking, the compiler "erases" them, and the bytecode is left with plain Object (or the bound from extends). This mechanism is called type erasure. The cast has not gone anywhere — it is simply the compiler that inserted it, not you.

It was done this way for compatibility with old code written before generics appeared (Java 5). But erasure has practical consequences worth knowing about.

At runtime, List<String> and List<Integer> are the very same type:

live example

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

public class Erasure {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();

        // at runtime both are just ArrayList
        System.out.println(strings.getClass() == integers.getClass()); // true
        System.out.println(strings.getClass().getName());              // java.util.ArrayList
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

You cannot check the type parameter with instanceof, and you cannot create an array of generics:

// if (obj instanceof List<String>) {}  // compile error
// T[] array = new T[10];               // not allowed — the type is erased

Short formula: generics are a contract for the compiler; at runtime there is no information about the concrete type left.

Generics in collections

Most often you meet generics precisely in collections — they are everywhere there. When you declare a collection, you fix the element type, and all the subsequent code becomes safe and readable:

live example

import java.util.HashMap;
import java.util.Map;

public class Ages {
    public static void main(String[] args) {
        Map<String, Integer> ages = new HashMap<>();   // key String, value Integer
        ages.put("Anna", 30);
        ages.put("Boris", 25);

        int age = ages.get("Anna");   // already Integer, no cast
        System.out.println(age + 1);
    }
}
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

The interfaces from the standard library are generic too, and when implementing them you substitute the type you need — for example, Comparable<T>:

record Person(String name, int age) implements Comparable<Person> {
    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

With var the type is still there: the variable gets the full generic type from the right-hand side, you just don't have to write it twice.

var scores = new HashMap<String, Integer>(); // the type is HashMap<String, Integer>

In short

  • Generics move type checking to compile time and remove manual casts ((String)).
  • A generic class declares a type parameter (class Box<T>); a bound is set with <T extends Number>.
  • A generic method declares its own type parameter before the return type; the type is usually inferred automatically.
  • Wildcards: ? extends — for reading (producer), ? super — for writing (consumer); the PECS rule.
  • Type erasure means that at runtime the concrete type is gone: you cannot do instanceof List<String> or new T[].
  • In collections generics are used everywhere — they are the main reason collections are safe.