Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Как работают дженерики Java: Дженерики

Тип данных определяет набор операций, который допустим для данного типа. Например, мы можем складывать числа, но не можем складывать булевы значения.

jshell> 3 + 4
$1 ==> 7

jshell> true + false
|  Error:
|  bad operand types for binary operator '+'
|    first type:  boolean
|    second type: boolean
|  true + false
|  ^----------^

Однако, логика кода не всегда зависит от того, с каким типом данных происходит работа. Ярким примером служат коллекции, где большая часть операций никак не связана с типом данных, который находится внутри. Например, операция добавления элемента в список никак не затрагивает сам элемент. То же самое касается изменения, удаления и большей части остальных операций. Эти операции производятся над самой коллекцией, но сами элементы никак не обрабатываются.

Представьте если бы коллекций в том виде, в котором мы с ними знакомились в Java не существовало. Как бы мы их реализовали? У нас было бы два способа:

Создать свой вариант коллекции для каждого типа данных

public class ArrayListOfInts {
    private Integer[] data;
    private int size;

    public ArrayListOfInts() {
        data = new Integer[10]; // initial capacity
        size = 0;
    }

    public void add(Integer value) {
        // Увеличивает размер внутреннего массива если место закончилось
        ensureCapacity();
        data[size++] = value;
    }

    public Integer get(int index) {
        return data[index];
    }

    // Остальные методы
}

Ровно такой же класс мы сделаем и для остальных типов. Разница в этих классах будет исключительно в типе данных, который указан в описании класса:

public class ArrayListOfStrings {
    private String[] data;
    private int size;

    public ArrayListOfStrings() {
        data = new String[10]; // initial capacity
        size = 0;
    }

    public void add(String value) {
        // Увеличивает размер внутреннего массива если место закончилось
        ensureCapacity();
        data[size++] = value;
    }

    public String get(int index) {
        return data[index];
    }

    // Остальные методы
}

Представьте, что, то же самое придется делать для каждого нового класса или интерфейса, которые добавляются в код. Никто не захочет тратить на это время. Поэтому обычно идут другим способом.

Приведение к Object

В Java все классы неявно наследуют класс Object. Тему наследования мы еще не проходили, но для данной задачи нам не нужны глубокие знания. Достаточно увидеть, что любой объект можно привести к типу Object, а можно выполнить обратное преобразование.

Object value = "string";
// Методы строки работать не будут
// Error: cannot find symbol symbol: method toUpperCase()
value.toUpperCase();
// Здесь у нас снова обычная строка
var value2 = (String) value;
// Этот код работает
value2.toUpperCase();

Таким образом мы можем создать ровно один класс, хранящий в себе все данные в виде Object.

public class ArrayListOfObjects {
    private Object[] data;
    private int size;

    public ArrayListOfObjects() {
        data = new Object[10]; // initial capacity
        size = 0;
    }

    public void add(Object value) {
        // Увеличивает размер внутреннего массива если место закончилось
        ensureCapacity();
        data[size++] = value;
    }

    // Данные придется преобразовывать в нужный тип снаружи
    public Object get(int index) {
        return data[index];
    }
    // Остальные методы
}

Использование:

var items = new ArrayListOfObjects();
items.add("Sun");
// Требуется ручное преобразование
var value = (String) items.get(0);

У этого способа есть серьезный недостаток, это необходимость вручную следить за типами и как следствие, отсутствие типобезопасности. В такую коллекцию можно добавить любые данные, так как все типы в Java являются подтипами Object.

var items = new ArrayListOfObjects();
items.add("Sun");
items.add(234);
items.add(true);
var value = (String) items.get(0);
var value = (Integer) items.get(1);
var value = (Boolean) items.get(2);
System.out.println(value);

Все это привело к тому, что в языке появились дженерики, которые с одной стороны убирают дублирование кода, с другой обеспечивают типобезопасность. Концепция дженериков основана на понятии "параметр типа". То есть у типа (класса или интерфейса) появляется параметр, который тоже является типом. Этот параметр определяет то, с каким типом будет работать дженерик для конкретной ситуации, например, созданного объекта. Синтаксически, параметр типа указывается в угловых скобках во время создания объекта из дженерика.

// items1 работает только с числами
var items1 = new MyArrayList<Integer>();
// items2 работает только со строками
var items2 = new MyArrayList<String>();

Внутри это выглядит примерно так:

public class MyArrayList<T> {
    private Object[] data;
    private int size;

    public MyArrayList() {
        data = new Object[10];
        size = 0;
    }

    public void add(T value) {
        data[size++] = value;
    }

    public T get(int index) {
        // Приведение типа к T
        return (T) data[index];
    }
    // Остальные методы
}

После названия класса ставятся угловые скобки, внутри которых используется имя для параметра типа. Обычно пишут T, но это не обязательно. Внутри класса параметр типа используется там, где бы использовался обычный тип. Единственное исключение в случае коллекций заключается в том, что данные все равно надо хранить как объекты. Преобразование делается во время получения данных, внутри дженерика.

Программирование с использованием дженериков часто называют обобщенным программированием, а сами дженерики параметризуемыми типами. Так как дженериками выступают классы и интерфейсы, в которые как в методы передается параметр, только в отличие от методов, параметром является не значение какого-то типа, а сам тип.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 21 ноября

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»
Изображение Тото

Задавайте вопросы, если хотите обсудить теорию или упражнения. Команда поддержки Хекслета и опытные участники сообщества помогут найти ответы и решить задачу