Учебное пособие по Java Generics - Что такое Generics и как их использовать?

Java Generics - одна из важнейших функций языка Java. Идея дженериков довольно проста, однако иногда она кажется сложной из-за отхода от обычного синтаксиса, связанного с ними.

Цель этого руководства - в простой для понимания форме познакомить вас с этой полезной концепцией дженериков.

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

Назначение Java Generics

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

List list = new ArrayList(); list.add('hey'); list.add(new Object());

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

Попробуйте повторить итерацию по приведенному выше списку.



for (int i=0; i< list.size(); i++) {
String value = (String) list.get(i); //CastClassException when i=1 }

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

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

С помощью дженериков вы можете явно объявить тип данных, который будет храниться при создании коллекции Java, как показано в следующем примере.

Примечание:Вы по-прежнему можете создать объект коллекции Java без указания типа хранимых данных, но это не рекомендуется. List stringList = new ArrayList();

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

stringList.add(new Integer(4)); //Compile time Error

Основная цель введения дженериков в Java состояла в том, чтобы избежать столкновения с ClassCastExceptions во время выполнения.

Создание Java Generics

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

Общий класс

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

public class GenericClass {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return this.item;
} }

Здесь T - параметр типа данных. T, N и E - это некоторые буквы, используемые для параметров типа данных в соответствии с соглашениями Java.

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

public static void main(String[] args) {
GenericClass gc1 = new GenericClass();
gc1.setItem('hello');
String item1 = gc1.getItem(); // 'hello'
gc1.setItem(new Object()); //Error
GenericClass gc2 = new GenericClass();
gc2.setItem(new Integer(1));
Integer item2 = gc2.getItem(); // 1
gc2.setItem('hello'); //Error }

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

Например:

GenericClass gc3 = new GenericClass(); //Error

Общие методы

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

public class GenericMethodClass {
public static void printItems(T[] arr){
for (int i=0; i< arr.length; i++) {

System.out.println(arr[i]);
}
}
public static void main(String[] args) {
String[] arr1 = {'Cat', 'Dog', 'Mouse'};
Integer[] arr2 = {1, 2, 3};

GenericMethodClass.printItems(arr1); // 'Cat', 'Dog', 'Mouse'
GenericMethodClass.printItems(arr2); // 1, 2, 3
} }

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

Параметры ограниченного типа

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

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

Например:

//accepts only subclasses of List public class UpperBoundedClass{
//accepts only subclasses of List
public void UpperBoundedMethod(T[] arr) {
} }

Здесь UpperBoundedClass и UpperBoundedMethod можно параметризовать только с помощью подтипов List тип данных.

List тип данных действует как верхняя граница параметра типа. Если вы попытаетесь использовать тип данных, который не является подтипом List, это вызовет ошибку времени компиляции.

Границы не ограничиваются только классами. Вы также можете передавать интерфейсы. В данном случае расширение интерфейса означает реализацию интерфейса.

Параметр также может иметь несколько границ, как показано в этом примере.

//accepts only subclasses of both Mammal and Animal public class MultipleBoundedClass{
//accepts only subclasses of both Mammal and Animal
public void MultipleBoundedMethod(T[] arr){
} }

Принимающий тип данных должен быть подклассом классов Animal и Mammal. Если одна из этих границ является классом, она должна быть первой в объявлении привязки.

В приведенном выше примере, если Mammal - это класс, а Animal - интерфейс, Mammal должен быть первым, как показано выше. В противном случае код выдает ошибку времени компиляции.

Подстановочные знаки Java Generics

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

public void printItems(List list) {
for (int i=0; i< list.size(); i++) {
System.out.println(list.get(i));
} }

Вышеуказанное printItems() принимает в качестве параметра списки любого типа данных. Это избавляет программистов от необходимости повторять коды для списков различных типов данных, что имело бы место без обобщений.

Подстановочные знаки с ограничением сверху

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

Пример:

public void printSubTypes(List list) {
for (int i=0; i< list.size(); i++) {
System.out.println(list.get(i));
} }

printSubTypes() принимает только списки, в которых хранятся подтипы Color. Он принимает список объектов RedColor или BlueColor, но не принимает список объектов Animal. Это потому, что Animal не является подтипом Color. Это пример подстановочного знака с ограничением сверху.

Подстановочные знаки с ограничением снизу

Аналогично, если бы у нас было:

public void printSuperTypes(List list) {
for (int i=0; i< list.size(); i++) {
System.out.println(list.get(i));
} }

тогда printSuperTypes() принимает только списки, в которых хранятся супертипы класса Dog. Он будет принимать список объектов Mammal или Animal, но не список объектов LabDog, потому что LabDog является не суперклассом Dog, а подклассом. Это пример подстановочного знака с ограничением снизу.

Заключение

Java Generics стал функцией, без которой программисты не могут жить с момента ее появления.

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

Чтобы стать экспертом в этом языке, важно хорошо разбираться в общих основах. Итак, применение того, что вы узнали из этого руководства, в практическом коде - это путь вперед.