Логотип Workflow

Article

Generics

Topic 9. Generics

Generics provide type-parameterized code: you write reusable logic while preserving compile-time type safety.

They solve two practical problems:

  1. reduce unsafe runtime casts,
  2. make APIs self-descriptive through type contracts.

Generics safety

Why generics matter

Without generics (raw types), type errors appear late:

List list = new ArrayList();
list.add("Alice");
list.add(100);
String name = (String) list.get(1); // ClassCastException at runtime

With generics, compiler catches wrong usage:

List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(100); // compile-time error
String first = names.get(0);

Basic syntax

  • T, E, K, V are conventional type parameter names.
  • List<String> means this list stores only strings.
  • Map<String, Integer> means string keys and integer values.

Common conventions:

  • T = Type,
  • E = Element,
  • K/V = Key/Value,
  • R = Result type.

Example 1. Generic method

public static <T> void printAll(List<T> items) {
    for (T item : items) {
        System.out.println(item);
    }
}

Usage:

printAll(List.of("A", "B", "C"));
printAll(List.of(1, 2, 3));

Example 2. Generic class

public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

Usage:

Box<String> textBox = new Box<>();
textBox.set("hello");
String text = textBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(42);
int number = intBox.get();

Example 3. extends and super wildcards

public static double sum(List<? extends Number> nums) {
    double total = 0.0;
    for (Number n : nums) {
        total += n.doubleValue();
    }
    return total;
}

public static void addDefaults(List<? super Integer> out) {
    out.add(10);
    out.add(20);
}

Meaning:

  • ? extends Number: safe read as Number, restricted writes.
  • ? super Integer: safe write of Integer, read as Object.

PECS rule

Producer Extends, Consumer Super:

  1. If structure produces values for you to read, use extends.
  2. If structure consumes values you write, use super.

Invariance in Java generics

List<Integer> is not a subtype of List<Number>.

List<Integer> ints = List.of(1, 2, 3);
// List<Number> nums = ints; // compile error

This prevents unsafe insertion of incompatible values.

Type erasure

Java generics are implemented through erasure:

  • type checks happen at compile time,
  • runtime mostly does not retain concrete generic arguments.

Practical implications:

  1. cannot do new T(),
  2. cannot reliably check instanceof List<String>,
  3. cannot create new List<String>[10].

Bounded type parameters

public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

T must support comparison with same type.

Multiple bounds are possible:

<T extends Number & Comparable<T>>

Generics + collections API design

Poor signature:

void copy(List<Object> dst, List<Object> src)

Better:

public static <T> void copy(List<? super T> dst, List<? extends T> src) {
    for (T item : src) {
        dst.add(item);
    }
}

This is reusable and type-safe.

Common beginner mistakes

  1. using raw types (List list),
  2. mixing up extends and super,
  3. forcing unchecked casts,
  4. overcomplicating type signatures unnecessarily.

Practical checklist

  1. Always parameterize collections.
  2. Use extends for read-oriented APIs.
  3. Use super for write-oriented APIs.
  4. Keep generic signatures readable.
  5. Add concise Javadoc when type contracts are non-trivial.

Key takeaway

Generics are a core quality mechanism: fewer runtime failures, clearer contracts, and safer reusable APIs.

Quiz

Check what you learned

Please login to pass quizzes.