Generics в Java: зачем нужны и как использовать
Generics (обобщения) — одна из ключевых возможностей языка Java, появившаяся в версии J2SE 5.0. Они позволяют создавать классы, интерфейсы и методы, которые работают с различными типами данных, сохраняя при этом строгую типизацию на этапе компиляции. В этой статье мы разберём, зачем нужны дженерики, как их правильно использовать и какие ошибки они помогают избежать.
Зачем нужны Generics в Java?
До появления обобщений программисты использовали приведение типов (casting) и работу с Object. Это приводило к двум основным проблемам:
- Отсутствие типобезопасности — в коллекцию можно было добавить объект любого типа, и ошибка проявлялась только в рантайме.
- Необходимость явного приведения — при извлечении элемента из коллекции требовался каст, что загромождало код.
// Без Generics (Java до 5)
List list = new ArrayList();
list.add("строка");
list.add(123); // Ошибка не на этапе компиляции
String s = (String) list.get(0); // Приходится явно приводить
// С Generics
List<String> list = new ArrayList<>();
list.add("строка");
// list.add(123); // Ошибка компиляции!
String s = list.get(0); // Приведение не нужно
Основные преимущества Generics:
- Типобезопасность — ошибки типов выявляются на этапе компиляции.
- Устранение явных приведений — код становится чище и читаемее.
- Повторное использование кода — одна реализация работает с разными типами.
Создание обобщённых классов и методов
Обобщённый класс
Чтобы создать класс, работающий с произвольным типом, достаточно указать параметр типа в угловых скобках после имени класса:
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public static void main(String[] args) {
Box<String> stringBox = new Box<>();
stringBox.setContent("Привет, Java!");
System.out.println(stringBox.getContent());
Box<Integer> intBox = new Box<>();
intBox.setContent(42);
System.out.println(intBox.getContent());
}
}
В этом примере T — параметр типа. При создании объекта Box<String> все вхождения T заменяются на String.
Обобщённый метод
Методы также могут быть параметризованы. Параметр типа указывается перед возвращаемым типом:
public class Util {
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C"};
printArray(intArray); // 1 2 3 4 5
printArray(strArray); // A B C
}
}
Обратите внимание: параметр типа <T> стоит перед void. Это обязательно для обобщённых методов.
Ограниченные типы (Bounded Type Parameters)
Иногда нужно ограничить, какие типы могут быть использованы в качестве параметра. Для этого используются ключевые слова extends и super.
Верхняя граница (extends)
Ограничивает тип сверху — можно использовать только сам указанный класс или его подклассы:
public class Calculator<T extends Number> {
private T value;
public Calculator(T value) {
this.value = value;
}
public double square() {
return value.doubleValue() * value.doubleValue();
}
public static void main(String[] args) {
Calculator<Integer> intCalc = new Calculator<>(5);
System.out.println(intCalc.square()); // 25.0
Calculator<Double> doubleCalc = new Calculator<>(3.14);
System.out.println(doubleCalc.square()); // 9.8596
// Calculator<String> error = new Calculator<>("test"); // Ошибка компиляции
}
}
Здесь T extends Number означает, что T может быть только Number или его подклассами (Integer, Double, Float и т.д.).
Wildcards (маски) — ? extends и ? super
Wildcards используются, когда точный тип неизвестен, но есть ограничения:
? extends T— тип, который является подтипомT(производитель данных).? super T— тип, который является супертипомT(потребитель данных).
Пример с List:
import java.util.*;
public class WildcardExample {
// Читаем из коллекции — используем ? extends
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// Записываем в коллекцию — используем ? super
public static void addIntegers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Integer> ints = Arrays.asList(1, 2, 3);
System.out.println(sumOfList(ints)); // 6.0
List<Number> nums = new ArrayList<>();
addIntegers(nums);
System.out.println(nums); // [1, 2, 3, 4, 5]
}
}
Это правило известно как PECS (Producer Extends, Consumer Super): если вы только читаете данные — используйте extends, если только записываете — super.
Type Erasure (стирание типов)
Важно понимать, что Generics в Java реализованы через стирание типов (type erasure). Это означает, что информация о параметрах типа существует только на этапе компиляции. В байт-коде все обобщённые типы заменяются на их границы (или Object, если граница не