DevGang
Авторизоваться

Как использовать Generics в Java – объяснено на примерах кода

В вашей Java-программе вы могли столкнуться с ужасным исключением ClassCastException во время выполнения при работе с различными типами объектов, такими как Integer, String и т. д. Эта ошибка чаще всего возникает из-за приведения объекта к неправильному типу данных.

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

Зачем нам нужны Generics?

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

List list = new ArrayList();

list.add("Hello");

String str = (String) list.get(0);

System.out.println("String: " + str);

Как видите, мы добавили объект String в ArrayList. Поскольку код написали мы, мы знаем, какой тип объекта представляет элемент, но компилятор этого не знает. Итак, когда мы пытаемся получить значение из списка, мы возвращаем объект и должны выполнить явное приведение типов.

list.add(123);

String number = (String) list.get(1);

System.out.println("Number: " + number);

Если мы добавим Integer в тот же список и попытаемся получить значение, мы получим исключение ClassCastException, поскольку объект Integer не может быть приведен к String.

Используя дженерики, мы можем решить обе проблемы, обсуждавшиеся выше. Давайте посмотрим, как это сделать.

Во-первых, нам нужно использовать оператор ромба и сузить тип объекта, содержащегося в этом списке. Нам нужно явно указать тип объекта внутри оператора ромба. Это обеспечит проверку во время компиляции, поэтому вам больше не придется выполнять явное приведение типов. Вы также сможете устранить исключение ClassCastException.

List<String> list = new ArrayList();

list.add("Hello");

String str = list.get(0); // No need for explicit casting

System.out.println("String: " + str);

list.add(123); // Throws compile-time error

Соглашения об именах для параметров типа

В предыдущем примере вы видели, что использование List<String> сузило тип объекта, который может содержать список. Ознакомьтесь со следующим примером класса Box и его работы с различными типами данных.

public class Box<T> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setValue("Hello, world!");
        System.out.println(stringBox.getValue());

        Box<Integer> integerBox = new Box<>();
        integerBox.setValue(123);
        System.out.println(integerBox.getValue());
    }
}

Обратите внимание, как объявлен класс Box<T>. Здесь T — параметр типа, указывающий, что класс Box может работать с любым объектом этого типа. То же самое проиллюстрировано в основном методе, где разрешено создание экземпляров Box<String> и Box<Integer>, что обеспечивает безопасность типов.

Согласно официальной документации:

По соглашению имена параметров типа представляют собой одиночные буквы верхнего регистра.

Наиболее часто используемые имена параметров типа:

  • E — элемент (широко используется в Java Collections Framework)
  • К - Ключ
  • Н – номер
  • Т-тип
  • В — Значение
  • S,U,V и т.д. - 2-й, 3-й, 4-й типы.

Давайте посмотрим, как можно написать универсальный метод. Ниже приводится соглашение:

public static <T> void printArray(T[] inputArr) {
	for (T element : inputArr) {
		System.out.print(element + " ");
	}
	System.out.println();
}

Здесь мы берем массив любого типа и печатаем его элементы. Обратите внимание, что вам необходимо указать параметр общего типа T в угловых скобках <> перед типом возвращаемого значения метода. Тело метода перебирает массив любого типа T, который мы передали в качестве параметра, и печатает каждый элемент.

 public static void main(String[] args) {
        // Create different arrays of type Integer, Double and Character
        Integer[] intArr = {1, 2, 3, 4, 5};
        Double[] doubleArr = {1.1, 2.2, 3.3, 4.4, 5.5};
        Character[] charArr = {'H', 'E', 'L', 'L', 'O'};

        System.out.println("Integer array contains:");
        printArray(intArr);   // pass an Integer array

        System.out.println("Double array contains:");
        printArray(doubleArr);   // pass a Double array

        System.out.println("Character array contains:");
        printArray(charArr);   // pass a Character array
    }

Мы можем вызвать этот общий метод, передав различные типы массивов (целое, двойное, символьное), и вы увидите, что ваша программа напечатает элементы каждого из этих массивов.

Ограничения на дженерики

В Generics мы используем границы для ограничения типов, которые может принимать универсальный класс, интерфейс или метод. Есть два типа:

1. Верхние границы

Это используется для ограничения универсального типа верхним пределом. Чтобы определить верхнюю границу, вы используете ключевое слово Extensions. Указывая верхнюю границу, вы гарантируете, что класс, интерфейс или метод принимает указанный тип и все его подклассы.

Синтаксис будет следующим: <T extends SuperClass>.

Если вы рассмотрите тот же класс Box, который мы использовали ранее, его можно изменить, как показано ниже:

class Box<T extends Number> {
    private T value;

    public void setValue(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}

В этом примере T может быть любым типом, расширяющим число, например Integer, Double или Float.

2. Нижние границы

Это используется для ограничения универсального типа нижним пределом. Чтобы определить нижнюю границу, вы используете ключевое слово super. Указывая нижнюю границу, вы гарантируете, что класс, интерфейс или метод принимает указанный тип и все его суперклассы.

Синтаксис будет следующим: <T super SubClass>.

Чтобы проиллюстрировать использование нижних границ, давайте рассмотрим следующий пример:

public static void printList(List<? super Integer> list) {
    for (Object element : list) {
        System.out.print(element + " ");
    }
    System.out.println();
}

Использование нижней границы <? super Integer> гарантирует, что вы можете передать указанный тип и все его суперклассы, которые в данном случае будут списком целых чисел, чисел или объектов, в метод printList.

Что такое подстановочные знаки?

? то, что вы видели в предыдущем примере, называется подстановочным знаком. Вы можете использовать их для ссылки на неизвестный тип.

Вы можете использовать подстановочный знак с верхней границей, и в этом случае он будет выглядеть примерно так: <? extends Number>. Его также можно использовать с нижней границей, например <? super Integer>.

Тип Стирание

Общий тип, который мы используем в нашем классе, интерфейсе или методе, доступен только во время компиляции и удаляется во время выполнения. Это сделано для обеспечения обратной совместимости, поскольку старые версии Java (до Java 1.5) ее не поддерживают.

Компилятор использует доступную ему информацию об универсальном типе для обеспечения безопасности типов. В процессе стирания типа:

  • Если тип не ограничен, параметры заменяются их границами или типом объекта.
  • Если тип ограничен, то параметры заменяются первой привязкой, а информация об универсальном типе будет удалена после компиляции.

Если мы посмотрим на пример класса Box:

public class Box<T> {
    private T value;
    //getters and setters
 }

Приведенный выше код станет таким:

public class Box {
    private Object value;
    //getters and setters
 }

Заключение

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

Источник:

#Java
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу