Generics в Java: зачем нужны и как использовать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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, если граница не

Блоги

Рекомендаций по книгам