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:
HELLOAn 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
3One 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: intUse the wrapper classes: List<Integer>, Map<String, Double>. Java boxes the numbers into objects and unboxes them back by itself.