Generics basics

Java · Practical · 10 min

Generics give compile-time type safety to every collection and most modern APIs.

Generics let a class or method work with a type that is chosen by the user. List<String> is a list that holds only strings: the compiler rejects add(42) and get returns a String with no cast. Before generics (Java 5), collections held Object and every read needed a cast that could fail at runtime.

The diamond <> lets the compiler infer type arguments: Map<String, List<Integer>> m = new HashMap<>();.

Type arguments must be reference types: write List<Integer>, not List<int>; autoboxing handles the conversion.

Writing your own generic class is simple — declare a type parameter after the name:

class Box<T> {
    private T value;
    void set(T v) { value = v; }
    T get() { return value; }
}

By convention type parameters are single capital letters: T (type), E (element), K/V (key/value), R (result).

A generic method declares its own type parameter before the return type: static <T> T firstOrNull(List<T> list). The compiler infers T from the arguments at each call site.

You can restrict a type parameter with a bound: <T extends Comparable<T>> means "any T that can compare itself to another T", which lets the method call compareTo.

Avoid raw types like plain List without a type argument. They exist only for compatibility with ancient code, disable the checks and produce "unchecked" warnings. Wildcards (? extends, ? super) and type erasure are covered in the advanced generics lesson.

Example

import java.util.List;

public class GenericsDemo {
    record Pair<A, B>(A first, B second) { }          // generic record

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

    static <T extends Comparable<T>> T max(List<T> list) {
        T best = list.get(0);
        for (T item : list) if (item.compareTo(best) > 0) best = item;
        return best;
    }

    public static void main(String[] args) {
        var p = new Pair<>("age", 42);                // Pair<String, Integer>
        int age = p.second();                         // no cast needed
        System.out.println(p + " " + age);            // Pair[first=age, second=42] 42
        System.out.println(firstOrDefault(List.<String>of(), "none")); // none
        System.out.println(max(List.of(3, 9, 4)));    // 9
        System.out.println(max(List.of("pear", "apple"))); // pear
    }
}

Remember

  • Type arguments give compile-time checks and remove casts
  • Use the diamond <> and let the compiler infer
  • Bounds like <T extends Comparable<T>> let generic code call methods on T

Watch out for

  • Using raw types (List list = new ArrayList();) and losing all type checking.

Check yourself

  1. Why does `List<int>` fail to compile?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/tutorial/java/generics/index.html