$ sudo teach IT
МОДУЛЬ 9 · УРОК 6

Generics — Обобщения

Зачем нужны обобщения, типизированные классы и методы, wildcard, type erasure, generic и массивы

~30 минут Для новичков Java

Проблема: 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 вопросов

Generic Box

Premium