Обобщения (generics)
Вы уже писали List<String> и Map<String, Integer>. Угловые скобки — это обобщения (generics): они позволяют написать класс или метод один раз и использовать его с разными типами, сохраняя проверку типов компилятором.
Зачем нужны обобщения
Представим коробку, в которой может лежать что угодно. Без обобщений её поле пришлось бы сделать типа Object, а при чтении приводить тип вручную — и ошибка обнаружится только при запуске:
class Box {
Object value;
}
void main() {
Box box = new Box();
box.value = "Привет";
Integer number = (Integer) box.value;
}Результат:
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')Обобщённый класс
Добавим к классу параметр типа T — имя, вместо которого при использовании подставят настоящий тип. Теперь компилятор знает, что лежит в коробке, и приведение не нужно:
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("Привет");
String text = box.get();
IO.println(text.toUpperCase());
}Результат:
ПРИВЕТПопытка положить в Box<String> число теперь не скомпилируется — ошибка видна сразу, а не у пользователя.
Несколько параметров
Параметров типа может быть несколько. По соглашению их называют одной заглавной буквой: T — тип, K и V — ключ и значение, E — элемент. Записи тоже бывают обобщёнными:
record Pair<A, B>(A first, B second) {}
void main() {
Pair<String, Integer> score = new Pair<>("Анна", 90);
IO.println(score.first() + ": " + score.second());
IO.println(score);
}Результат:
Анна: 90
Pair[first=Анна, second=90]Обобщённый метод
Параметр типа можно объявить и у отдельного метода — перед типом результата. При вызове Java сама выводит T из аргументов:
<T> T last(List<T> items) {
return items.get(items.size() - 1);
}
void main() {
String name = last(List.of("Анна", "Олег"));
Integer number = last(List.of(1, 2, 3));
IO.println(name);
IO.println(number);
}Результат:
Олег
3Один метод работает со списком строк, чисел и чего угодно ещё, и результат каждый раз имеет правильный тип.
Где ещё встречаются обобщения
Почти вся стандартная библиотека обобщённая: коллекции, Optional, Comparator, функциональные интерфейсы для лямбд. Когда вы пишете Comparator<Product>, вы подставляете тип в параметр T интерфейса Comparator<T>. Поэтому читать запись вида Map<K, V> в документации стоит уметь с самого начала.
Частая ошибка
В параметр типа можно подставить только ссылочный тип — класс, запись, интерфейс. Примитивы вроде int и double не подходят:
void main() {
List<int> numbers = new ArrayList<>();
}Результат:
Main.java:2: error: unexpected type
List<int> numbers = new ArrayList<>();
^
required: reference
found: intИспользуйте классы-обёртки: List<Integer>, Map<String, Double>. Java сама упакует числа в объекты и распакует обратно.