1. Проблема «сирих» колекцій (raw types)
Ненадовго зануримося в історію. До Java 5 усі колекції були «всеїдними». Вони зберігали обʼєкти типу Object, і компілятор не перевіряв, що саме ви туди кладете. Хочете покласти рядок? Будь ласка. Число? Чому ні. Кота? Теж можна.
// Приклад "сирих" колекцій (raw types), Java до 5-ї версії
List list = new ArrayList();
list.add("Привіт");
list.add(42);
list.add(new Object());
Проблема виявлялася під час «діставання» та використання значення:
String s = (String) list.get(0); // OK, це рядок
String s2 = (String) list.get(1); // БУМ! ClassCastException
Компілятор мовчить, а під час виконання ви отримуєте ClassCastException. Це як коробка з написом «яблука», у якій лежать чашка, банан і їжак.
Чому це погано?
- Помилки виявляються лише під час виконання.
- Плутаються типи: доводиться вручну кастувати обʼєкти до потрібного типу (cast).
- Код менш читабельний і небезпечніший.
Рішення — generics (узагальнення)
Generics (узагальнення) — механізм, який дає змогу створювати класи, інтерфейси та методи з параметрами типу. Тобто ви кажете колекції: «Зберігай лише рядки», а компілятор суворо стежить за цим.
List<String> words = new ArrayList<>();
words.add("Привіт");
words.add("Світ");
// words.add(42); // Помилка компіляції! Не можна додати int до List<String>
Тепер компілятор не дозволить покласти до списку нічого, окрім рядків. Помилку буде виявлено ще до запуску програми.
Головна ідея generics:
Забезпечити типобезпеку колекцій (і не лише), щоб помилки ловилися на етапі компіляції, а не під час виконання.
2. Синтаксис generics: як це виглядає в коді
Зазначення типу в кутових дужках
Коли створюєте колекцію, зазначайте тип елементів у <>:
List<String> names = new ArrayList<>();
names.add("Аліса");
names.add("Боб");
// names.add(123); // Помилка: не можна додати число до списку рядків
String first = names.get(0); // Кастування не потрібне!
Класика:
- List<String> — список рядків
- List<Integer> — список цілих чисел
- Set<Double> — множина дійсних чисел
- Map<String, Integer> — ключ String, значення Integer
Чому не можна писати просто List?
Можна, але ви втрачаєте всі переваги generics, і компілятор попередить:
List list = new ArrayList(); // raw type — не рекомендується!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // Привіт, ClassCastException!
Сучасний Java‑код завжди використовує generics.
Діамантовий оператор <>
Починаючи з Java 7, можна не вказувати тип праворуч, якщо він зрозумілий з контексту:
List<String> list = new ArrayList<>(); // Компілятор сам зрозуміє, що тут <String>
3. Generics для різних колекцій
Приклади для List, Set, Map
List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Аліса");
uniqueNames.add("Боб");
Map<String, Integer> ages = new HashMap<>();
ages.put("Аліса", 23);
ages.put("Боб", 31);
Приклад із власним класом
class Student {
String name;
int age;
// ...
}
List<Student> students = new ArrayList<>();
students.add(new Student());
4. Корисні нюанси
Переваги generics
Типобезпека. Компілятор стежить, щоб до колекції потрапляли лише елементи потрібного типу.
Немає потреби у кастуванні. Раніше: String s = (String) list.get(0);. Тепер: String s = list.get(0);.
Код читабельніший і надійніший. Менше сюрпризів під час виконання.
Обмеження generics
Неможливо використовувати примітивні типи. Узагальнення працюють лише з обʼєктами, а не з примітивами (int, double, boolean). Використовуйте класи-обгортки: Integer, Double, Boolean.
List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int автоматично перетворюється на Integer (autoboxing)
Коротко про стирання типів (type erasure)
У Java generics реалізовані через механізм стирання типів: після компіляції інформація про параметри типу стирається, і під час виконання JVM не знає, що це був List<String>, а не просто List. Це зроблено заради зворотної сумісності.
Наслідок: не можна перевіряти параметр типу через instanceof із конкретним аргументом типу.
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // Помилка компіляції!
Спроба додати елемент іншого типу — помилка компіляції
List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // Помилка компіляції: incompatible types: int cannot be converted to String
Map<String, Integer> map = new HashMap<>();
map.put("Кіт", 5);
// map.put(3, "Слон"); // Помилка: ключ має бути String, значення — Integer
І це прекрасно: помилки ловляться на етапі компіляції.
Не лише колекції
Generics можна використовувати у власних класах і методах. Наприклад, універсальна «Коробка»:
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("Привіт");
System.out.println(stringBox.get());
Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());
У колекціях generics — стандарт, але ви зустрінете їх і в інших місцях, наприклад, у Stream API та Optional.
5. Типові помилки під час роботи з generics
Помилка № 1: Використання «сирих» колекцій. Запис виду List list = new ArrayList(); позбавляє типобезпеки. Завжди зазначайте параметри типу, наприклад List<String>.
Помилка № 2: Спроба використовувати примітиви. Не можна написати List<int>, використовуйте List<Integer>.
Помилка № 3: Ручне кастування під час читання з колекції. Якщо ви використовуєте generics, кастування на кшталт (String) list.get(i) не потрібне. Якщо доводиться — десь порушили типи.
Помилка № 4: Очікування, що параметри типів доступні під час виконання. Через стирання типів не можна перевіряти їх через instanceof на кшталт List<String>.
Помилка № 5: Змішування різних типів в одній колекції. Якщо оголошено List<String>, не намагайтеся додати Integer — компілятор не пропустить, і це добре.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ