Generics — Обобщения
Зачем нужны обобщения, типизированные классы и методы, wildcard, type erasure, generic и массивы
Проблема: Object
До появления обобщений (generics) в коллекциях использовался тип Object. Это означало, что коллекция могла хранить что угодно:
import java.util.ArrayList;
import java.util.List;
public class NoGenericsDemo {
public static void main(String[] args) {
List names = new ArrayList();
names.add("Алиса");
names.add("Боб");
names.add(42);
String name = (String) names.get(2);
System.out.println(name);
}
}
Вывод программы:
Exception in thread "main" java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String
Мы добавили число в список имён, и программа упала при попытке прочитать его как строку. Ошибка обнаружилась только во время выполнения!
Generics решают эту проблему: вы указываете тип данных при компиляции, и компилятор проверяет правильность на этапе компиляции.
Аналогия: Generics — это как типизированная коробка. Без generics — коробка для «чего угодно»: можно положить и книгу, и тарелку, и кирпич. С generics — «коробка для книг»: положить кирпич не даст компилятор.
Типизированный класс
Параметр типа указывается в угловых скобках <T>:
public class Box<T> {
private T content;
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
@Override
public String toString() {
return "Box{" + content + "}";
}
}
public class BoxDemo {
public static void main(String[] args) {
Box<String> stringBox = new Box<>("Привет");
Box<Integer> intBox = new Box<>(42);
Box<Boolean> boolBox = new Box<>(true);
System.out.println(stringBox);
System.out.println(intBox);
System.out.println(boolBox);
String value = stringBox.getContent();
System.out.println("Значение: " + value);
}
}
Вывод программы:
Box{Привет}
Box{42}
Box{true}
Значение: Привет
Как видите, мы создали три коробки с разными типами. При попытке положить не тот тип — компилятор выдаст ошибку:
Box<String> box = new Box<>("Текст");
box.setContent(42); // ОШИБКА КОМПИЛЯЦИИ!
Несколько параметров типа
Класс может иметь несколько параметров типа:
public record Pair<K, V>(K key, V value) {
public String toString() {
return "(" + key + ", " + value + ")";
}
}
public class PairDemo {
public static void main(String[] args) {
Pair<String, Integer> nameAge = new Pair<>("Алиса", 25);
Pair<Integer, Boolean> idActive = new Pair<>(42, true);
System.out.println(nameAge);
System.out.println(idActive);
}
}
Вывод программы:
(Алиса, 25)
(42, true)
Типизированный метод
Параметр типа можно объявить только для метода:
public class GenericMethodDemo {
public static <T> void printArray(T[] array) {
for (T item : array) {
System.out.print(item + " ");
}
System.out.println();
}
public static <T> T getFirst(T[] array) {
if (array == null || array.length == 0) {
return null;
}
return array[0];
}
public static <T> boolean contains(T[] array, T element) {
for (T item : array) {
if (item.equals(element)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Integer[] numbers = {1, 2, 3, 4, 5};
String[] names = {"Алиса", "Боб", "Виктор"};
printArray(numbers);
printArray(names);
System.out.println("Первое число: " + getFirst(numbers));
System.out.println("Первое имя: " + getFirst(names));
System.out.println("Содержит 3: " + contains(numbers, 3));
System.out.println("Содержит 'Боб': " + contains(names, "Боб"));
}
}
Вывод программы:
1 2 3 4 5
Алиса Боб Виктор
Первое число: 1
Первое имя: Алиса
Содержит 3: true
Содержит 'Боб': true
Параметр типа <T> объявляется перед возвращаемым типом метода.
Ограничения типов (bounded types)
Иногда нужно ограничить, какие типы могут быть использованы. Для этого используется extends:
public class MathUtils {
// T должен быть числом (Number или его наследник)
public static <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
// T должен реализовывать Comparable
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
public static void main(String[] args) {
System.out.println("Сумма: " + sum(5, 3.5));
System.out.println("Сумма: " + sum(10L, 20L));
System.out.println("Максимум: " + max(10, 20));
System.out.println("Максимум: " + max("Алиса", "Боб"));
}
}
Вывод программы:
Сумма: 8.5
Сумма: 30.0
Максимум: 20
Максимум: Боб
Попробуйте вызвать sum("а", "б") — компилятор выдаст ошибку, потому что String не наследует от Number.
Аналогия: Bounded types — как фильтр на входе в клуб. В клуб пускают только тех, кому есть 18 (extends Number) или у кого есть VIP-карта (extends Comparable). Остальных — компилятор не впустит.
Wildcard (?)
Wildcard ? используется, когда вы не знаете или не заботитесь о типе:
import java.util.ArrayList;
import java.util.List;
public class WildcardDemo {
// Upper bounded: только Number и его наследники
public static double sumList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
// Lower bounded: только Integer и его предки (Number, Object)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Unbounded: любой тип
public static void printAll(List<?> list) {
for (Object item : list) {
System.out.print(item + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> integers = List.of(1, 2, 3, 4, 5);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
List<Number> numbers = List.of(1, 2.5, 3L);
System.out.println("Сумма integers: " + sumList(integers));
System.out.println("Сумма doubles: " + sumList(doubles));
System.out.println("Сумма numbers: " + sumList(numbers));
List<Number> numList = new ArrayList<>();
addNumbers(numList);
System.out.println("Числа: " + numList);
printAll(List.of("Привет", 42, true));
}
}
Вывод программы:
Сумма integers: 15.0
Сумма doubles: 7.5
Сумма numbers: 6.5
Числа: [1, 2, 3]
Привет 42 true
| Wildcard | Читать | Записывать |
|---|---|---|
| List<?> | Object | Ничего (кроме null) |
| List<? extends T> | T | Ничего |
| List<? super T> | Object | T и его наследники |
Type Erasure
При компиляции Java стирает информацию о типе. List<String> и List<Integer> в bytecode становятся просто List.
import java.util.ArrayList;
import java.util.List;
public class TypeErasureDemo {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());
System.out.println("Тип: " + strings.getClass().getName());
}
}
Вывод программы:
true
Тип: java.util.ArrayList
Это означает:
- Нельзя создать
new T()— компилятор не знает, какой класс - Нельзя проверить
instanceof List<String> - Нельзя создать массив
new T[10]
Generics и массивы
Java не позволяет создавать массивы обобщённых типов. Вот как обходить это ограничение:
import java.lang.reflect.Array;
import java.util.Arrays;
public class GenericArrayDemo {
@SuppressWarnings("unchecked")
public static <T> T[] createArray(Class<T> type, int size) {
return (T[]) Array.newInstance(type, size);
}
public static <T> void printArray(T[] array) {
System.out.println(Arrays.toString(array));
}
public static void main(String[] args) {
Integer[] numbers = createArray(Integer.class, 5);
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
printArray(numbers);
String[] words = createArray(String.class, 3);
words[0] = "Привет";
words[1] = "Мир";
printArray(words);
}
}
Вывод программы:
[1, 2, 3, null, null]
[Привет, Мир, null]
Практический пример: стек
import java.util.ArrayList;
import java.util.List;
public class Stack<T> {
private final List<T> elements = new ArrayList<>();
private final int maxSize;
public Stack(int maxSize) {
this.maxSize = maxSize;
}
public void push(T item) {
if (elements.size() >= maxSize) {
throw new RuntimeException("Стек переполнен!");
}
elements.add(item);
}
public T pop() {
if (elements.isEmpty()) {
throw new RuntimeException("Стек пуст!");
}
return elements.remove(elements.size() - 1);
}
public T peek() {
if (elements.isEmpty()) {
throw new RuntimeException("Стек пуст!");
}
return elements.get(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
public int size() {
return elements.size();
}
}
public class StackDemo {
public static void main(String[] args) {
Stack<String> nameStack = new Stack<>(5);
nameStack.push("Алиса");
nameStack.push("Боб");
nameStack.push("Виктор");
System.out.println("Верхний: " + nameStack.peek());
System.out.println("Размер: " + nameStack.size());
while (!nameStack.isEmpty()) {
System.out.println("Извлекли: " + nameStack.pop());
}
}
}
Вывод программы:
Верхний: Виктор
Размер: 3
Извлекли: Виктор
Извлекли: Боб
Извлекли: Алиса
Именование параметров типа
По conventions используются следующие имена:
T - Type (тип)
E - Element (элемент)
K - Key (ключ)
V - Value (значение)
N - Number (число)
R - Return (возвращаемый тип)
Итоги урока
Итоги урока
- Generics обеспечивают типобезопасность на этапе компиляции
- Параметры типа указываются в угловых скобках:
<T> - Могут использоваться для классов, интерфейсов и методов
extendsограничивает тип сверхуsuperограничивает тип снизу- Wildcard
?используется для неизвестных типов - Type erasure — параметры типа стираются при компиляции
- Нельзя создавать
new T(),new T[10],instanceof T - Рекомендуемые имена: T, E, K, V, N
Модуль 9 завершён! →
Тест: Generics
5 вопросов