What you'll learn

  • Why generics exist
  • Writing generic classes and methods
  • Bounded types with extends
  • Wildcards and the PECS rule

You’ve already used generics: List<String> is a list that can only hold Strings. Generics let a class or method work with any type, while the compiler still checks that you use it consistently. They’re everywhere in modern Java — collections, Optional<T>, Function<T, R>, streams — so reading them comfortably is essential before the advanced lessons.

Why they exist

Before generics (Java 5), a List held plain Objects:

List list = new ArrayList();
list.add("AX-200");
String sku = (String) list.get(0);   // cast needed, and a wrong cast only fails at runtime

With generics the compiler knows what’s inside, so no cast is needed and mistakes are caught at compile time:

List<String> skus = new ArrayList<>();
skus.add(42);                        // compile error: not a String

A generic class

static class Box<T> {
    private final T value;
    Box(T value) { this.value = value; }
    T get() { return value; }
}

Box<String> name = new Box<>("Acme");
Box<Integer> qty = new Box<>(24);
name.get().toUpperCase();            // the compiler knows it's a String

T is a type parameter — a placeholder filled in when you use the class. Common names: T (type), E (element), K/V (key/value), R (result).

Records can be generic too:

record Pair<A, B>(A first, B second) { }
Pair<String, Double> line = new Pair<>("AX-200", 8.50);

A generic method

static <T> T firstOrDefault(List<T> list, T fallback) {
    return list.isEmpty() ? fallback : list.get(0);
}

The <T> before the return type declares the type parameter for this method only. Java infers T from the arguments.

Bounded types

Sometimes you need some capability from T. extends sets an upper bound:

static <T extends Number> double sum(List<T> numbers) {
    double total = 0;
    for (T n : numbers) total += n.doubleValue();   // allowed: every Number has doubleValue()
    return total;
}

Wildcards: ?

List<Integer> is not a List<Number>, even though Integer is a Number — otherwise you could add a Double to a list of Integers. Wildcards express “a list of some subtype”:

static void printAll(List<? extends Number> numbers) { ... }   // read Numbers from it
static void addDefaults(List<? super Integer> target) { ... }  // put Integers into it

A handy rule is PECS: Producer Extends, Consumer Super. If a collection produces values for you to read, use ? extends T; if it consumes values you write, use ? super T. You’ll see exactly this in the signatures of Collections.sort, Comparator and the stream methods.

The full example

// GenericsDemo.java — generic classes, generic methods and bounded types.
import java.util.ArrayList;
import java.util.List;

public class GenericsDemo {

    // A generic class: T is decided by whoever uses it
    static class Box<T> {
        private final T value;
        Box(T value) { this.value = value; }
        T get() { return value; }
        boolean isPresent() { return value != null; }
    }

    // A generic pair with two type parameters
    record Pair<A, B>(A first, B second) { }

    // A generic method: works for any type T
    static <T> T firstOrDefault(List<T> list, T fallback) {
        return list.isEmpty() ? fallback : list.get(0);
    }

    // Bounded type: only numbers allowed
    static <T extends Number> double sum(List<T> numbers) {
        double total = 0;
        for (T n : numbers) {
            total += n.doubleValue();
        }
        return total;
    }

    // Wildcard: read from a list of any subtype of Number
    static void printAll(List<? extends Number> numbers) {
        numbers.forEach(n -> System.out.print(n + " "));
        System.out.println();
    }

    public static void main(String[] args) {
        Box<String> name = new Box<>("Acme");
        Box<Integer> qty = new Box<>(24);
        System.out.println(name.get().toUpperCase() + " " + (qty.get() + 1));

        Pair<String, Double> line = new Pair<>("AX-200", 8.50);
        System.out.println(line.first() + " costs " + line.second());

        System.out.println(firstOrDefault(new ArrayList<String>(), "none"));   // none
        System.out.println(sum(List.of(1, 2, 3.5)));                           // 6.5
        printAll(List.of(1, 2, 3));
    }
}

Try it yourself

Write a generic method <T> List<T> repeat(T value, int times) that returns a list containing value times times.

Show a solution
static <T> List<T> repeat(T value, int times) {
    List<T> result = new ArrayList<>();
    for (int i = 0; i < times; i++) {
        result.add(value);
    }
    return result;
}
// repeat("ok", 3) -> [ok, ok, ok]; repeat(7, 2) -> [7, 7]

Key takeaways

  • Generics give you reusable code that is still type-checked.
  • <T> on a class or method declares a type parameter.
  • <T extends X> requires a capability; wildcards make APIs flexible.
  • PECS: producers extends, consumers super.

Example files