Java · Topic 1 of 8

Generics

Card 1 of 14

What you will learn

Generics let classes and methods work with many types while the compiler checks them, so you avoid casts and catch mistakes early. Under the hood Java erases type arguments, which explains most of the odd rules around wildcards, arrays and instanceof.

Why generics exist

Before generics, collections held Object and you cast on every read. A wrong cast only failed at runtime.

With List<String> the compiler checks every add and get, and inserts the casts for you. Type arguments must be reference types, so use Integer, not int.

Example

Why generics exist

List<String> names = new ArrayList<>();
names.add("Asha");
// names.add(42);        // compile error
String first = names.get(0); // no cast

Generic classes and methods

A class declares type parameters in angle brackets: class Box<T>. A method can declare its own, placed before the return type.

Use bounds to require capabilities. <T extends Comparable<T>> means T can be compared with itself. Multiple bounds use &, with a class first.

Example

Generic classes and methods

static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}
int m = max(3, 7);   // T inferred as Integer

Invariance and wildcards

List<String> is not a List<Object>, even though String is an Object. If it were, you could add an Integer through the Object view.

Wildcards add flexibility. ? extends T is for reading (producer), ? super T is for writing (consumer). This is PECS. With List<?> you can read Objects but only add null.

Example

Invariance and wildcards

static double sum(List<? extends Number> xs) {
    double t = 0;
    for (Number n : xs) t += n.doubleValue();
    return t;
}
static void fill(List<? super Integer> out) {
    out.add(1);
}

Type erasure

The compiler checks generics, then erases them. A type parameter becomes its leftmost bound, or Object, and casts are inserted where needed. At runtime List<String> and List<Integer> are both just ArrayList.

Bridge methods keep overriding working after erasure, for example when you implement Comparable<Person>.

Example

Type erasure

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true

What erasure forbids

Since T does not exist at runtime, you cannot write new T(), new T[n], or x instanceof T. You also cannot overload two methods that differ only by type argument, or declare a generic subclass of Throwable.

Workarounds: pass a Class<T> or Supplier<T>, use ArrayList<T> instead of T[], and use List<?> in instanceof checks.

Raw types and heap pollution

A raw type such as List switches generic checks off. Mixing raw and generic code can put the wrong object behind List<String> (heap pollution).

The failure then appears later as a ClassCastException at an inserted cast, far from the real bug. Treat unchecked warnings as bugs, and use @SafeVarargs only on methods that cannot be overridden.

Key points

  • Generics give compile-time type safety and remove manual casts.
  • Type arguments must be reference types; use Integer, not int.
  • Generics are invariant: List<String> is not a List<Object>.
  • PECS: use ? extends to read from a source, ? super to write into a sink.
  • Type erasure removes type arguments at runtime, which causes most generic restrictions.
  • Avoid raw types and unchecked casts to prevent heap pollution.

Common mistakes

  • Using raw types like List and ignoring unchecked warnings.
  • Expecting List<Integer> to be assignable to List<Number>.
  • Trying to write new T(), new T[10], or instanceof List<String>.
  • Adding elements to a List<? extends T> or reading typed values from List<? super T>.
  • Using <T extends Comparable<T>> when <T extends Comparable<? super T>> is needed for subclasses.

Lesson complete

Ready to check what you learned?

Swipe left or right, or use the buttons.