Зачем нужны generics
Без обобщений коллекция хранит Object, и ошибки типов обнаруживаются только во время выполнения:
import java.util.ArrayList;import java.util.List;public class WithoutGenerics { public static void main(String[] args) { List list = new ArrayList(); list.add("строка"); list.add(42); String s = (String) list.get(1); }}Последняя строка бросит ClassCastException. Обобщения переносят проверку на этап компиляции:
import java.util.ArrayList;import java.util.List;public class WithGenerics { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("строка"); String s = list.get(0); }}Добавить в List<String> число компилятор не позволит.
Обобщенный класс
public class Box<T> { private T value; public Box(T value) { this.value = value; } public T get() { return value; } public void set(T value) { this.value = value; }}public class Main { public static void main(String[] args) { Box<String> text = new Box<>("привет"); Box<Integer> number = new Box<>(42); String s = text.get(); int n = number.get(); System.out.println(s + " " + n); }}Параметр типа нельзя заменить примитивом: Box<int> недопустим, используйте Box<Integer>.
Несколько параметров
public class Pair<A, B> { private final A first; private final B second; public Pair(A first, B second) { this.first = first; this.second = second; } public A getFirst() { return first; } public B getSecond() { return second; } public Pair<B, A> swap() { return new Pair<>(second, first); } @Override public String toString() { return "(" + first + ", " + second + ")"; } public static void main(String[] args) { Pair<String, Integer> pair = new Pair<>("возраст", 30); System.out.println(pair); System.out.println(pair.swap()); }}Принятые имена параметров: T — type, E — element, K — key, V — value, R — result.
Обобщенные методы
import java.util.List;public class Utils { public static <T> T first(List<T> list) { if (list.isEmpty()) { throw new IllegalArgumentException("Список пуст"); } return list.get(0); } public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } public static void main(String[] args) { System.out.println(first(List.of("a", "b"))); System.out.println(first(List.of(1, 2, 3))); String[] words = {"один", "два", "три"}; swap(words, 0, 2); System.out.println(String.join(", ", words)); }}Компилятор выводит тип автоматически.
Ограничения типов
extends задает верхнюю границу:
import java.util.List;public class Bounds { public static <T extends Comparable<T>> T max(List<T> list) { T best = list.get(0); for (T item : list) { if (item.compareTo(best) > 0) { best = item; } } return best; } public static <T extends Number> double sum(List<T> numbers) { double total = 0; for (T n : numbers) { total += n.doubleValue(); } return total; } public static void main(String[] args) { System.out.println(max(List.of(3, 9, 2))); System.out.println(max(List.of("яблоко", "груша", "слива"))); System.out.println(sum(List.of(1, 2, 3))); System.out.println(sum(List.of(1.5, 2.5))); }}Несколько границ: <T extends Number & Comparable<T>>.
Wildcard
Знак ? обозначает неизвестный тип.
import java.util.ArrayList;import java.util.List;public class Wildcards { static void printAll(List<?> list) { for (Object o : list) { System.out.println(o); } } static double sum(List<? extends Number> list) { double total = 0; for (Number n : list) { total += n.doubleValue(); } return total; } static void fill(List<? super Integer> list) { for (int i = 0; i < 3; i++) { list.add(i); } } public static void main(String[] args) { printAll(List.of("a", 1, 2.5)); System.out.println(sum(List.of(1, 2, 3))); List<Number> numbers = new ArrayList<>(); fill(numbers); System.out.println(numbers); }}Правило PECS: Producer Extends, Consumer Super.
- из списка
? extends Tможно только читать; - в список
? super Tможно только писать.
Инвариантность
List<Integer> не является подтипом List<Number>, хотя Integer — подтип Number. Иначе в список целых можно было бы положить Double. Для гибкости используйте wildcard.
Стирание типов
После компиляции информация об обобщенных параметрах удаляется. Отсюда ограничения:
- нельзя создать
new T()иnew T[10]; - нельзя проверить
instanceof List<String>; - нельзя перегрузить методы, отличающиеся только параметром типа;
- нельзя создавать статические поля типа
T.
import java.util.ArrayList;import java.util.List;public class Erasure { public static void main(String[] args) { List<String> strings = new ArrayList<>(); List<Integer> numbers = new ArrayList<>(); System.out.println(strings.getClass() == numbers.getClass()); }}Вывод: true.
Обобщенные интерфейсы
public interface Repository<T, ID> { void save(T item); T findById(ID id);}import java.util.HashMap;import java.util.Map;public class UserRepository implements Repository<User, Integer> { private final Map<Integer, User> storage = new HashMap<>(); @Override public void save(User user) { storage.put(user.id(), user); } @Override public User findById(Integer id) { return storage.get(id); }}public record User(int id, String name) {}Пример: собственный стек
import java.util.ArrayList;import java.util.List;public class Stack<T> { private final List<T> items = new ArrayList<>(); public void push(T item) { items.add(item); } public T pop() { if (items.isEmpty()) { throw new IllegalStateException("Стек пуст"); } return items.remove(items.size() - 1); } public T peek() { if (items.isEmpty()) { throw new IllegalStateException("Стек пуст"); } return items.get(items.size() - 1); } public boolean isEmpty() { return items.isEmpty(); } public int size() { return items.size(); } public static void main(String[] args) { Stack<String> stack = new Stack<>(); stack.push("a"); stack.push("b"); System.out.println(stack.pop()); System.out.println(stack.peek()); }}