Що таке дженерики (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
- ❌ “Примітивні типи можна використовувати як параметри” — Тільки wrapper класи (Integer, Double)
Пов’язані теми:
- [[12. У чому переваги використання дженериків]]
- [[13. Що таке type erasure (стирання типів)]]
- [[15. Що таке bounded type parameters]]
- [[18. Чи можна використовувати примітивні типи як параметри дженериків]]