Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Generics в Java
Generics (обобщения) — это один из самых важных, но часто неправильно понимаемых механизмов Java. Ключ к пониманию — это понять, что они работают совсем не так, как в других языках вроде C#.
Основная идея: Type Safety
// Без generics - нужны кастинги
List list = new ArrayList();
list.add("Hello");
list.add(123);
String str = (String) list.get(0); // Нужен кастинг
int num = (int) list.get(1); // И здесь
// С generics - всё типизировано
List<String> list = new ArrayList<String>();
list.add("Hello");
// list.add(123); // Ошибка компиляции!
String str = list.get(0); // Без кастинга
Type Erasure: Главная особенность
Этот момент потрясает много разработчиков. Java удаляет информацию о типах на этапе компиляции!
// Исходный код
public class Container<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// После компиляции (type erasure) - это то, что реально выполняется
public class Container {
private Object value; // T заменён на Object (upper bound)
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
Почему это произошло? Для backward compatibility с Java 1.4 и раньше.
Type Parameters
Простые параметры:
public class Box<T> {
private T item;
public void set(T item) { this.item = item; }
public T get() { return item; }
}
// Использование
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String str = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(42);
int num = intBox.get();
Несколько параметров:
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
// Использование
Pair<String, Integer> pair = new Pair<>("Age", 30);
Bounded Type Parameters
Upper Bound (наследники определённого класса):
// T должен быть Number или его наследник
public class NumberBox<T extends Number> {
private T number;
public double doubleValue() {
return number.doubleValue(); // Доступны методы Number
}
}
// Использование
NumberBox<Integer> intBox = new NumberBox<>();
NumberBox<Double> doubleBox = new NumberBox<>();
// NumberBox<String> stringBox = new NumberBox<>(); // Ошибка компиляции!
// Множественные bounds
public class GenericClass<T extends Comparable<T> & Cloneable> {
// T должен быть Comparable И Cloneable
}
Lower Bound (суперклассы):
public void processList(List<? super Integer> list) {
list.add(42); // OK - можно добавлять Integer
// Integer val = list.get(0); // Ошибка! На выходе Object
}
// Использование
List<Number> numbers = new ArrayList<>();
processList(numbers); // OK - Number это суперкласс Integer
List<Object> objects = new ArrayList<>();
processList(objects); // OK - Object это суперкласс Integer
Wildcards (?)
Covariance (поведение как массивы):
// Можно читать, но не писать
public void processList(List<? extends Number> list) {
Number num = list.get(0); // OK - читаем
// list.add(42); // Ошибка компиляции! (PECS: Producer Extends)
}
// Это работает
List<Integer> integers = Arrays.asList(1, 2, 3);
processList(integers); // OK
List<Double> doubles = Arrays.asList(1.0, 2.0);
processList(doubles); // OK
Contravariance:
// Можно писать, но не читать
public void processList(List<? super Integer> list) {
list.add(42); // OK - пишем
// Integer val = list.get(0); // Ошибка! Результат Object (PECS: Consumer Super)
}
List<Number> numbers = new ArrayList<>();
processList(numbers); // OK
List<Object> objects = new ArrayList<>();
processList(objects); // OK
Unbounded wildcard:
// Работает с любым типом, но не типизировано
public void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Использование
printList(Arrays.asList("a", "b")); // OK
printList(Arrays.asList(1, 2, 3)); // OK
printList(Arrays.asList(true, false)); // OK
PECS: Producer Extends, Consumer Super
Это самый важный принцип работы с wildcards:
// Producer (источник данных) - используй extends
public void copy(List<? extends Number> source, List<Number> dest) {
for (Number num : source) { // Читаю из source
dest.add(num);
}
}
// Consumer (приёмник данных) - используй super
public void reverse(List<Object> source, List<? super Object> dest) {
for (int i = source.size() - 1; i >= 0; i--) {
dest.add(source.get(i)); // Пишу в dest
}
}
Generic Methods
// Метод с собственными type параметрами
public <T> T getRandomElement(List<T> list) {
return list.get(new Random().nextInt(list.size()));
}
// Использование
String str = getRandomElement(Arrays.asList("a", "b", "c"));
Integer num = getRandomElement(Arrays.asList(1, 2, 3));
// Bounded generic method
public <T extends Comparable<T>> T max(List<T> list) {
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
Type Erasure: Проблемы
Проблема 1: Невозможно различить типы на runtime
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// Это НЕ работает!
if (stringList instanceof List<String>) { // Ошибка компиляции!
// ...
}
// Это работает только для raw type
if (stringList instanceof List) { // OK
// Но мы не знаем, это List<String> или List<Integer>
}
Проблема 2: Нельзя создавать массивы generics
// Это не работает!
List<String>[] array = new List<String>[10]; // Ошибка компиляции!
// Почему? Потому что на runtime это становится:
List[] array = new List[10]; // Потеря информации о типе
// Решение: используй List of List
List<List<String>> list = new ArrayList<>();
Проблема 3: Cannot throw generic exception
// Не работает
public <T extends Exception> void execute() throws T { // Ошибка!
// ...
}
// Но можно обойти через reflection
public <T extends Exception> void execute(Class<T> exceptionClass) throws T {
try {
// Какой-то код
} catch (Exception e) {
throw exceptionClass.cast(e);
}
}
Практические примеры
Generic interface:
public interface Repository<T> {
T findById(Long id);
List<T> findAll();
void save(T entity);
void delete(T entity);
}
public class UserRepository implements Repository<User> {
@Override
public User findById(Long id) { /* ... */ }
@Override
public List<User> findAll() { /* ... */ }
// ...
}
Safe variance:
// Неправильно (не компилируется)
List<Number> numbers = new ArrayList<Integer>(); // Ошибка!
// Почему? Потому что это небезопасно:
List<Number> numbers = new ArrayList<Integer>();
numbers.add(3.14); // Добавляем Double, но внутри Integer!
// Правильно - используй wildcard
List<? extends Number> numbers = new ArrayList<Integer>();
Лучшие практики
- Используй generics везде — не используй raw types
- Помни про type erasure — нельзя обращаться к информации о типе на runtime
- Следуй PECS — Producer Extends, Consumer Super
- Используй bounded type parameters когда нужны методы типа
- Избегай raw types — включи @SuppressWarnings("rawtypes") если нужно
- Протестируй edge cases — generics часто вызывают невидимые ошибки
Заключение
Generics в Java — это compile-time feature. На runtime всё становится Object. Это сделано для backward compatibility, но может вызывать неожиданные поведения. Важно понимать, что происходит за кулисами.