Параметризованные типы (Generics) в Java позволяют создавать классы, интерфейсы и методы, которые работают с различными типами данных, не потеряв при этом типовую безопасность. Это позволяет избежать дублирования кода и сделать его более универсальным и гибким. Generics позволяют писать код, который может работать с любыми типами данных, при этом гарантируя, что типы будут проверяться во время компиляции.
Основы Generics
1. Параметризация классов
С помощью параметризированных типов можно создавать классы, которые принимают параметр типа, вместо того чтобы жестко задавать его. Это позволяет использовать один класс для работы с различными типами данных.
Пример: Параметризированный класс:
// Класс с параметризированным типом T
class Box<T> {
private T value;
// Устанавливаем значение
public void setValue(T value) {
this.value = value;
}
// Получаем значение
public T getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
// Создаем объект Box для типа Integer
Box<Integer> intBox = new Box<>();
intBox.setValue(42);
System.out.println("Integer value: " + intBox.getValue());
// Создаем объект Box для типа String
Box<String> strBox = new Box<>();
strBox.setValue("Hello, Generics!");
System.out.println("String value: " + strBox.getValue());
}
}
В этом примере:
- Класс Box
использует параметр типа T
. Это означает, что мы можем создать объект Box
с любым типом, например, Integer
, String
и т.д.
- Мы передаем конкретный тип при создании экземпляра Box<Integer>
, Box<String>
и т.д.
2. Параметризация методов
Методы также могут быть параметризованы, что позволяет работать с различными типами данных в рамках одного метода.
Пример: Параметризированный метод:
public class Util {
// Параметризированный метод
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3};
String[] strArray = {"Hello", "World"};
// Метод принимает массив с любым типом
printArray(intArray); // Выведет: 1 2 3
printArray(strArray); // Выведет: Hello World
}
}
В этом примере:
- Метод printArray
параметризован типом T
, что позволяет использовать его с массивами любого типа (в примере — Integer[]
и String[]
).
- Ключевое слово <T>
обозначает параметр типа, который будет передан методу, и используется внутри метода.
3. Ограничения типов (Bounded Types)
Вы можете ограничить типы, которые могут быть использованы в параметризированных классах или методах, с помощью ограничений типов (bounded types). Это делается с использованием ключевого слова extends
.
Пример: Ограничение типа:
// Ограничиваем тип, чтобы он был подклассом Number
class NumberBox<T extends Number> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
NumberBox<Integer> intBox = new NumberBox<>();
intBox.setValue(42);
System.out.println("Integer value: " + intBox.getValue());
// Ошибка компиляции, так как String не является наследником Number
// NumberBox<String> strBox = new NumberBox<>(); // Ошибка
}
}
В этом примере:
- Класс NumberBox
ограничивает тип T
типами, которые являются подклассами Number
(например, Integer
, Double
, Float
и т.д.).
- Попытка создать объект NumberBox<String>
приведет к ошибке компиляции, потому что String
не является наследником Number
.
4. Несколько параметров типа
Вы можете использовать несколько параметров типа в одном классе или методе.
Пример: Несколько параметров типа:
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;
}
}
public class Main {
public static void main(String[] args) {
// Создаем пару, где K - String, а V - Integer
Pair<String, Integer> pair = new Pair<>("Age", 30);
System.out.println(pair.getKey() + ": " + pair.getValue());
}
}
Здесь:
- Мы используем два параметра типа K
и V
в классе Pair
, что позволяет работать с любыми типами ключей и значений.
5. Wildcards (Подстановочные знаки)
Подстановочные знаки (wildcards) позволяют использовать более гибкие типы при работе с параметризированными типами. Основные виды wildcard:
- ?
— универсальный wildcard, который означает любой тип.
- ? extends T
— ограничение верхнего диапазона, означает, что тип должен быть T
или его подтипом.
- ? super T
— ограничение нижнего диапазона, означает, что тип должен быть T
или его суперклассом.
Пример использования wildcard:
// Метод, который принимает список объектов типа Number или его подтипов
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
public class Main {
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
// Работает с любыми типами, которые наследуют Number
printNumbers(intList); // Выведет: 1 2 3
printNumbers(doubleList); // Выведет: 1.1 2.2 3.3
}
}
В этом примере:
- Метод printNumbers
принимает список любого типа, который является наследником Number
(например, Integer
, Double
, Float
и т.д.).
Преимущества использования Generics
- Типовая безопасность: Generics обеспечивают проверку типов на этапе компиляции, что помогает избежать ошибок времени выполнения, связанных с некорректными типами.
- Универсальность: Одинаковый код может работать с любыми типами данных, что снижает количество дублирующегося кода.
- Читаемость: Код с Generics обычно более читаем и легко поддерживаем, поскольку явно указаны типы данных, с которыми работают классы и методы.
Резюме
- Generics позволяют параметризовать классы, методы и интерфейсы, делая их универсальными и типобезопасными.
- С помощью ограничений типов можно уточнить, какие типы могут быть использованы.
- Wildcards (подстановочные знаки) дают возможность работать с обобщениями, не привязываясь к конкретным типам, с помощью гибких ограничений.