Что такое дженерики (Generics) в Java
Основная идея: вы пишете код один раз, а он работает с любым типом.
🟢 Junior Level
Дженерики — это механизм в Java, позволяющий создавать классы, интерфейсы и методы, которые работают с разными типами, сохраняя при этом типовую безопасность.
Основная идея: вы пишете код один раз, а он работает с любым типом.
// Без дженериков (до Java 5)
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // нужен cast!
// С дженериками (Java 5+)
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // без cast!
Зачем нужны:
- Безопасность типов — ошибка обнаружится на этапе компиляции
- Не нужен cast — код чище и безопаснее
- Переиспользование — один код для разных типов
Пример:
// Один класс работает с любым типом
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
Box<Integer> intBox = new Box<>();
intBox.set(42);
🟡 Middle Level
Как это работает
Type parameter (<T>) — это placeholder для типа, который указывается при использовании:
// Объявление
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// Использование
Box<String> box = new Box<>(); // T = String
box.set("Hello");
String s = box.get(); // автоматически String, без cast
Несколько параметров:
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", 25);
Bounded Type Parameters
Можно ограничить тип:
// Только Number и его наследники
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() { return value.doubleValue(); }
}
NumberBox<Integer> intBox = new NumberBox<>(); // ✅
NumberBox<String> strBox = new NumberBox<>(); // ❌ ошибка
Типичные ошибки
- Использование raw types: ```java // ❌ Raw type — нет безопасности типов List list = new ArrayList(); list.add(“Hello”); list.add(42); // компилятор не ругается, но это баг!
// ✅ Параметризированный тип
List
2. **Ожидание работы с примитивами:**
```java
// ❌ Примитивы нельзя использовать как параметры типа
List<int> list = new ArrayList<>(); // compilation error
// ✅ Используйте wrapper классы
List<Integer> list = new ArrayList<>();
Практическое применение
1. Generic методы:
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
List<String> strings = List.of("a", "b", "c");
String first = getFirst(strings); // T выводится как String
2. Generic интерфейсы:
public interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
}
public class UserRepository implements Repository<User, Long> {
public Optional<User> findById(Long id) { /* ... */ }
}
🔴 Senior Level
Internal Implementation
Type Erasure (стирание типов):
// Исходный код
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// После компиляции (type erasure)
public class Box {
private Object value; // T -> Object (upper bound)
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}
// Компилятор добавляет cast при использовании
Box<String> box = new Box<>();
box.set("Hello");
String s = (String) box.get(); // неявный cast
Bounded type erasure:
public class NumberBox<T extends Number> {
private T value;
}
// После erasure: T -> Number (первый bound)
public class NumberBox {
private Number value;
}
Архитектурные Trade-offs
Type Erasure:
| Плюсы | Минусы |
|---|---|
| Бинарная совместимость (Java 5 код работает с Java 1.4) | Нельзя создать new T() |
| No runtime overhead | Нельзя проверить obj instanceof T |
Проблемы с массивами (new T[]) |
|
| Bridge methods для сохранения полиморфизма |
// Bridge Method — синтетический метод, который компилятор добавляет // для сохранения полиморфизма после стирания типов.
Edge Cases
1. Generic array creation:
// ❌ Нельзя создать массив дженерик-типа
public class Box<T> {
private T[] array = new T[10]; // compilation error
}
// ✅ Решение — массив Object с cast
public class Box<T> {
private T[] array = (T[]) new Object[10];
// ⚠️ Опасно: если вернуть этот массив наружу, caller может получить ClassCastException.
// ArrayList использует Object[] внутри, но контролирует все записи — это безопасно.
// Ваш код — не всегда.
}
2. Instanceof с дженериками:
// ❌ Нельзя
if (obj instanceof Box<String>) { } // compilation error
// ✅ Можно — raw type или wildcard
if (obj instanceof Box<?>) { } // OK
3. Static контекст:
public class Box<T> {
// ❌ Static поле не может использовать T
private static T value; // compilation error
// ✅ Generic метод — свой T
public static <E> void process(E item) { }
}
Производительность
Type erasure overhead:
- Runtime: Zero overhead (никаких дополнительных проверок)
- Memory: Нет дополнительных данных
- Cast: Неявные cast при доступе (negligible)
Box<Integer> box = new Box<>();
box.set(42); // autoboxing int -> Integer
Integer val = box.get(); // cast + unboxing
Total overhead: autoboxing + cast (~1-2 ns)
Production Experience
Generic Repository:
public interface BaseEntity {
Long getId();
}
public interface Repository<T extends BaseEntity, ID extends Serializable> {
Optional<T> findById(ID id);
List<T> findAll(Pageable pageable);
<S extends T> S save(S entity);
long count();
boolean existsById(ID id);
}
public abstract class JpaRepository<T extends BaseEntity, ID extends Serializable>
implements Repository<T, ID> {
private final Class<T> entityType;
private final EntityManager em;
protected JpaRepository(Class<T> entityType, EntityManager em) {
this.entityType = entityType;
this.em = em;
}
public Optional<T> findById(ID id) {
return Optional.ofNullable(em.find(entityType, id));
}
}
Best Practices
// ✅ Используйте дженерики для коллекций
List<User> users = new ArrayList<>();
// ✅ Generic методы для переиспользования
public static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
return list.stream().filter(predicate).toList();
}
// ✅ Bounded types для ограничения
public static <T extends Comparable<T>> T max(List<T> list) {
return list.stream().max(Comparable::compareTo).orElse(null);
}
// ❌ Raw types
// ❌ new T[] или new T()
// ❌ instanceof T
🎯 Шпаргалка для интервью
Обязательно знать:
- Дженерики появились в Java 5 (JEP 14) для type safety при работе с коллекциями
- Type parameters:
<T>,<K, V>,<E>— placeholder-ы для типов - Bounded type parameters:
<T extends Number>— ограничение типа - Type erasure — компилятор удаляет информацию о типах, T -> Object или bound
- Raw types — использование без type argument, отключает проверку типов
- Примитивы нельзя использовать:
List<int>— ошибка, нуженList<Integer>
Частые уточняющие вопросы:
- Зачем нужны дженерики? — Compile-time type safety, elimination of casts, code reuse
- Что такое type erasure? — После компиляции T заменяется на Object или bound, JVM не видит дженерики
- Можно ли instanceof T? — Нет,
obj instanceof T— compilation error из-за type erasure - **Почему List
не наследует List
Красные флаги (НЕ говорить):
- ❌ “Дженерики работают в runtime” — Type erasure удаляет типы при компиляции
- ❌ “List
— подтип List - ❌ “Можно создать new T()” — Type erasure не позволяет узнать конструктор T
- ❌ “Primite types можно использовать как параметры” — Только wrapper классы (Integer, Double)
Связанные темы:
- [[12. В чём преимущества использования дженериков]]
- [[13. Что такое type erasure (стирание типов)]]
- [[15. Что такое bounded type parameters]]
- [[18. Можно ли использовать примитивные типы как параметры дженериков]]