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); // ОК, это строка
String s2 = (String) list.get(1); // БУМ! ClassCastException
Компилятор молчит, а в рантайме вы получаете ClassCastException. Это как коробка с надписью «яблоки», в которой лежат чашка, банан и ёжик.
Почему это плохо?
- Ошибки проявляются только во время выполнения.
- Путаются типы: приходится вручную приводить объекты к нужному типу (cast).
- Код менее читаемый и более опасный.
Решение — дженерики (обобщения)
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.
Diamond-оператор <>
С 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 — компилятор не пропустит, и это хорошо.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ