Вопрос 11 · Раздел 20

Что такое дженерики (Generics) в Java

Основная идея: вы пишете код один раз, а он работает с любым типом.

Версии по языкам: English Russian Ukrainian

🟢 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!

Зачем нужны:

  1. Безопасность типов — ошибка обнаружится на этапе компиляции
  2. Не нужен cast — код чище и безопаснее
  3. Переиспользование — один код для разных типов

Пример:

// Один класс работает с любым типом
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<>();   // ❌ ошибка

Типичные ошибки

  1. Использование raw types: ```java // ❌ Raw type — нет безопасности типов List list = new ArrayList(); list.add(“Hello”); list.add(42); // компилятор не ругается, но это баг!

// ✅ Параметризированный тип List list = new ArrayList<>(); list.add("Hello"); // list.add(42); // ошибка компиляции


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?** — Дженерики инвариантны, для гибкости используются wildcards

Красные флаги (НЕ говорить):

  • ❌ “Дженерики работают в runtime” — Type erasure удаляет типы при компиляции
  • ❌ “List — подтип List" — Дженерики инвариантны
  • ❌ “Можно создать new T()” — Type erasure не позволяет узнать конструктор T
  • ❌ “Primite types можно использовать как параметры” — Только wrapper классы (Integer, Double)

Связанные темы:

  • [[12. В чём преимущества использования дженериков]]
  • [[13. Что такое type erasure (стирание типов)]]
  • [[15. Что такое bounded type parameters]]
  • [[18. Можно ли использовать примитивные типы как параметры дженериков]]