Питання 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
  • ❌ “Примітивні типи можна використовувати як параметри” — Тільки wrapper класи (Integer, Double)

Пов’язані теми:

  • [[12. У чому переваги використання дженериків]]
  • [[13. Що таке type erasure (стирання типів)]]
  • [[15. Що таке bounded type parameters]]
  • [[18. Чи можна використовувати примітивні типи як параметри дженериків]]