Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Generics

You have already written List<String> and Map<String, Integer>. The angle brackets are generics: they let you write a class or method once and use it with different types while keeping the compiler's type checks.

Why generics are needed

Imagine a box that can hold anything. Without generics its field would have to be of type Object, and you would have to cast the type by hand when reading — and a mistake would show up only at run time:

class Box {
    Object value;
}
 
void main() {
    Box box = new Box();
    box.value = "Hello";
    Integer number = (Integer) box.value;
}

Output:

Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')

A generic class

Let us add a type parameter T to the class — a name that is replaced with a real type when the class is used. Now the compiler knows what is in the box, and no cast is needed:

class Box<T> {
    private T value;
 
    void put(T value) {
        this.value = value;
    }
 
    T get() {
        return value;
    }
}
 
void main() {
    Box<String> box = new Box<>();
    box.put("Hello");
    String text = box.get();
    IO.println(text.toUpperCase());
}

Output:

HELLO

An attempt to put a number into a Box<String> will no longer compile — the mistake is visible right away, not to the user.

Several parameters

There can be several type parameters. By convention they are named with a single capital letter: T for a type, K and V for key and value, E for an element. Records can be generic too:

record Pair<A, B>(A first, B second) {}
 
void main() {
    Pair<String, Integer> score = new Pair<>("Anna", 90);
    IO.println(score.first() + ": " + score.second());
    IO.println(score);
}

Output:

Anna: 90
Pair[first=Anna, second=90]

A generic method

A type parameter can also be declared on a single method — before the return type. When calling it, Java infers T from the arguments by itself:

<T> T last(List<T> items) {
    return items.get(items.size() - 1);
}
 
void main() {
    String name = last(List.of("Anna", "Oleg"));
    Integer number = last(List.of(1, 2, 3));
    IO.println(name);
    IO.println(number);
}

Output:

Oleg
3

One method works with a list of strings, of numbers and of anything else, and the result has the right type every time.

Where else generics appear

Almost the whole standard library is generic: collections, Optional, Comparator, the functional interfaces for lambdas. When you write Comparator<Product>, you substitute a type for the T parameter of the Comparator<T> interface. That is why it pays to be able to read notation like Map<K, V> in the documentation from the very start.

Common mistake

Only a reference type — a class, record or interface — can be substituted for a type parameter. Primitives such as int and double do not fit:

void main() {
    List<int> numbers = new ArrayList<>();
}

Output:

Main.java:2: error: unexpected type
    List<int> numbers = new ArrayList<>();
         ^
  required: reference
  found:    int

Use the wrapper classes: List<Integer>, Map<String, Double>. Java boxes the numbers into objects and unboxes them back by itself.

Thanks for reading!

Donate to the site — we work hard for you.