Обобщения Java – одна из самых важных функций, введенных в Java 5. Если вы работали с коллекциями Java и версией 5 или выше, я уверен, что вы ее использовали. Обобщения в Java с коллекциями очень просты, но они предоставляют намного больше возможностей, чем просто создание типа коллекции. Мы попытаемся изучить функции обобщений в этой статье. Понимание обобщений иногда может стать запутанным, если мы используем специальную терминологию, поэтому я постараюсь объяснить это просто и понятно.
Мы рассмотрим следующие темы обобщений в Java.
1. Обобщения в Java
Обобщения были добавлены в Java 5 для обеспечения проверки типов на этапе компиляции и удаления риска возникновения исключения ClassCastException, которое часто возникало при работе с коллекциями. Весь фреймворк коллекций был переписан с использованием обобщений для обеспечения типобезопасности. Давайте посмотрим, как обобщения помогают нам безопасно использовать коллекции.
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
for(Object obj : list){
// Приведение типов, приводящее к исключению ClassCastException на этапе выполнения
String str=(String) obj;
}
Приведенный выше код компилируется без ошибок, но вызывает исключение ClassCastException на этапе выполнения, потому что мы пытаемся привести объект в списке к типу String, тогда как один из элементов имеет тип Integer. После Java 5 мы используем классы коллекций следующим образом.
List list1 = new ArrayList(); // java 7 ? List list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); //ошибка компиляции
for(String str : list1){
//не требуется приведение типов, избегается исключение ClassCastException
}
Обратите внимание, что при создании списка мы указали, что тип элементов в списке будет String. Поэтому, если мы попытаемся добавить в список любой другой тип объекта, программа выдаст ошибку на этапе компиляции. Также обратите внимание, что в цикле for нам не нужно приводить тип элемента в списке, следовательно, исключается исключение ClassCastException на этапе выполнения.
2. Обобщенный класс Java
Мы можем определять собственные классы с обобщенными типами. Обобщенный тип – это класс или интерфейс, параметризованный типами. Мы используем угловые скобки (<>) для указания параметра типа. Чтобы понять выгоду, давайте представим, что у нас есть простой класс, как показано ниже:
package com.journaldev.generics;
public class GenericsTypeOld {
private Object t;
public Object get() {
return t;
}
public void set(Object t) {
this.t = t;
}
public static void main(String args[]){
GenericsTypeOld type = new GenericsTypeOld();
type.set("Pankaj");
String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
}
}
Обратите внимание, что при использовании этого класса нам приходится использовать приведение типов, и это может вызвать ClassCastException во время выполнения. Теперь мы будем использовать обобщенный класс Java, чтобы переписать тот же класс, как показано ниже.
package com.journaldev.generics;
public class GenericsType<T> {
private T t;
public T get(){
return this.t;
}
public void set(T t1){
this.t=t1;
}
public static void main(String args[]){
GenericsType<String> type = new GenericsType<>();
type.set("Pankaj"); //valid
GenericsType type1 = new GenericsType(); //raw type
type1.set("Pankaj"); //valid
type1.set(10); //valid and autoboxing support
}
}
Обратите внимание на использование класса GenericsType в методе main. Нам не нужно делать приведение типов, и мы можем избежать ClassCastException во время выполнения. Если мы не предоставим тип во время создания, компилятор выдаст предупреждение о том, что “GenericsType является необработанным типом. Ссылки на обобщенный тип GenericsType<T> должны быть параметризованы”. Когда мы не предоставляем тип, тип становится Object
, и поэтому он допускает как объекты String, так и Integer. Однако всегда стоит стараться избегать этого, потому что нам придется использовать приведение типов при работе с необработанным типом, что может вызвать ошибки во время выполнения.
Совет: Мы можем использовать аннотацию @SuppressWarnings("rawtypes")
, чтобы подавить предупреждение компилятора, ознакомьтесь с учебником по аннотациям Java.
Также обратите внимание, что он поддерживает автоупаковку Java.
3. Обобщенный интерфейс Java
Интерфейс Comparable – отличный пример использования обобщений в интерфейсах и записывается следующим образом:
package java.lang;
import java.util.*;
public interface Comparable<T> {
public int compareTo(T o);
}
Аналогичным образом мы можем создавать обобщенные интерфейсы в Java. Мы также можем иметь несколько параметров типа, как в интерфейсе Map. Снова мы можем предоставить параметризованное значение для параметризованного типа, например new HashMap<String, List<String>>();
является допустимым.
4. Обобщенный тип Java
Соглашение о наименовании обобщенного типа Java помогает нам легко понимать код, и иметь соглашение о наименовании – одна из лучших практик языка программирования Java. Таким образом, обобщенные типы также имеют свои собственные соглашения об именовании. Обычно имена параметров типа представляют собой одиночные прописные буквы, чтобы их легко можно было отличить от переменных Java. Самые часто используемые имена параметров типа:
- E – Element (used extensively by the Java Collections Framework, for example ArrayList, Set etc.)
- K – Key (Used in Map)
- N – Number
- T – Type
- V – Value (Used in Map)
- S,U,V etc. – 2nd, 3rd, 4th types
5. Обобщенный метод Java
Иногда нам не хочется параметризовывать всю класс, в таком случае мы можем создать обобщенный метод в Java. Поскольку конструктор является особым типом метода, мы также можем использовать обобщенные типы в конструкторах. Вот класс, показывающий пример обобщенного метода в Java.
package com.journaldev.generics;
public class GenericsMethods {
//Обобщенный метод в Java
public static boolean isEqual(GenericsType g1, GenericsType g2){
return g1.get().equals(g2.get());
}
public static void main(String args[]){
GenericsType g1 = new GenericsType<>();
g1.set("Pankaj");
GenericsType g2 = new GenericsType<>();
g2.set("Pankaj");
boolean isEqual = GenericsMethods.isEqual(g1, g2);
//Вышеприведенное утверждение можно записать просто как
isEqual = GenericsMethods.isEqual(g1, g2);
//Эта функция, известная как вывод типов, позволяет вызывать обобщенный метод как обычный метод, без указания типа между угловыми скобками.
//Компилятор определит необходимый тип
}
}
Обратите внимание на сигнатуру метода isEqual, показывающую синтаксис использования обобщенных типов в методах. Также обратите внимание, как использовать эти методы в нашей программе на Java. Мы можем указать тип при вызове этих методов, или мы можем вызывать их как обычный метод. Компилятор Java достаточно умён, чтобы определить тип переменной для использования, эта возможность называется вывод типов.
6. Ограничения обобщенных типов в Java
Предположим, мы хотим ограничить тип объектов, которые могут быть использованы в параметризованном типе, например, в методе, который сравнивает два объекта, и мы хотим убедиться, что принимаемые объекты являются сравнимыми. Чтобы объявить ограниченный тип параметра, перечислите имя параметра типа, за которым следует ключевое слово extends, за которым следует его верхний предел, аналогично приведенному ниже методу.
public static <T extends Comparable<T>> int compare(T t1, T t2){
return t1.compareTo(t2);
}
Вызов этих методов аналогичен неограниченному методу, за исключением того, что если мы попытаемся использовать любой класс, который не является Comparable, будет сгенерирована ошибка времени компиляции. Ограниченные типы параметров могут использоваться как с методами, так и с классами и интерфейсами. Обобщенные типы Java также поддерживают несколько ограничений, т.е. <T extends A & B & C>. В этом случае A может быть интерфейсом или классом. Если A – класс, то B и C должны быть интерфейсами. У нас не может быть более одного класса в нескольких ограничениях.
7. Обобщенные типы Java и наследование
Мы знаем, что наследование в Java позволяет нам присваивать переменной A другую переменную B, если A является подклассом B. Поэтому мы можем подумать, что любой обобщенный тип A может быть присвоен обобщенному типу B, но это не так. Давайте рассмотрим это на простом примере.
package com.journaldev.generics;
public class GenericsInheritance {
public static void main(String[] args) {
String str = "abc";
Object obj = new Object();
obj=str; // works because String is-a Object, inheritance in java
MyClass myClass1 = new MyClass();
MyClass
Мы не можем присвоить переменной MyClass
8. Обобщенные классы и подтипы в Java
Мы можем создавать подтипы обобщенного класса или интерфейса, расширяя или реализуя его. Отношения между типовыми параметрами одного класса или интерфейса и типовыми параметрами другого определяются с помощью ключевых слов extends и implements. Например, ArrayList
interface MyList<E,T> extends List<E>{
}
Подтипами List
9. Маски обобщений в Java
Знак вопроса (?) является подстановочным символом в обобщениях и представляет неизвестный тип. Подстановочный символ может использоваться как тип параметра, поля или локальной переменной, а также иногда как возвращаемый тип. Мы не можем использовать подстановочные символы при вызове обобщенного метода или создании экземпляра обобщенного класса. В следующих разделах мы узнаем о верхних ограниченных подстановочных символах, нижних ограниченных подстановочных символах и захвате подстановочных символов.
9.1) Верхние ограниченные подстановочные символы Java Generics
Верхние ограниченные подстановочные символы используются для смягчения ограничения на тип переменной в методе. Предположим, мы хотим написать метод, который будет возвращать сумму чисел в списке, поэтому наша реализация будет что-то вроде этого.
public static double sum(List<Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
Теперь проблема с вышеприведенной реализацией заключается в том, что она не будет работать с List of Integers или Doubles, потому что мы знаем, что List<Integer> и List<Double> не связаны, вот когда полезен верхний ограниченный подстановочный символ. Мы используем подстановочный символ обобщений с ключевым словом extends и классом или интерфейсом верхней границы, который позволит нам передавать аргумент верхней границы или его подклассы. Вышеприведенную реализацию можно изменить, как в приведенной ниже программе.
package com.journaldev.generics;
import java.util.ArrayList;
import java.util.List;
public class GenericsWildcards {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
ints.add(3); ints.add(5); ints.add(10);
double sum = sum(ints);
System.out.println("Sum of ints="+sum);
}
public static double sum(List<? extends Number> list){
double sum = 0;
for(Number n : list){
sum += n.doubleValue();
}
return sum;
}
}
Это похоже на написание нашего кода в терминах интерфейса, в вышеприведенном методе мы можем использовать все методы класса верхней границы Number. Обратите внимание, что с верхней ограниченной списком мы не имеем права добавлять в список ничего, кроме null. Если мы попытаемся добавить элемент в список внутри метода sum, программа не скомпилируется.
9.2) Дженерики Java Неограниченная подстановка подстановочных символов
Иногда у нас есть ситуация, когда мы хотим, чтобы наш обобщенный метод работал со всеми типами, в этом случае можно использовать неограниченную подстановку. Это то же самое, что и использование <? extends Object>.
public static void printData(List<?> list){
for(Object obj : list){
System.out.print(obj + "::");
}
}
Мы можем предоставить аргумент типа List<String> или List<Integer> или любой другой тип списка объектов методу printData. Аналогично ограниченному сверху списку, нам не разрешается ничего добавлять в список.
9.3) Дженерики Java Нижняя ограниченная подстановка
Предположим, мы хотим добавлять целые числа в список целых чисел в методе, мы можем сохранить тип аргумента как List
public static void addIntegers(List<? super Integer> list){
list.add(new Integer(50));
}
10. Подтипирование с использованием универсального подстановочного знака
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
11. Java Generics Type Erasure
Generics в Java были добавлены для обеспечения проверки типов во время компиляции, и они не используются во время выполнения, поэтому компилятор Java использует функцию type erasure для удаления всего кода проверки типов в байт-коде и вставки приведения типов при необходимости. Type erasure обеспечивает то, что не создаются новые классы для параметризованных типов; в результате, использование generics не влечет за собой накладных расходов во время выполнения. Например, если у нас есть generic класс, как показано ниже;
public class Test<T extends Comparable<T>> {
private T data;
private Test<T> next;
public Test(T d, Test<T> n) {
this.data = d;
this.next = n;
}
public T getData() { return this.data; }
}
Компилятор Java заменяет ограниченный тип параметра T первым интерфейсом границы, Comparable, как показано в следующем коде:
public class Test {
private Comparable data;
private Test next;
public Node(Comparable d, Test n) {
this.data = d;
this.next = n;
}
public Comparable getData() { return data; }
}
12. Часто задаваемые вопросы о обобщениях
12.1) Зачем мы используем обобщения в Java?
Обобщения обеспечивают строгую проверку типов во время компиляции и снижают риск возникновения ClassCastException и явного приведения типов объектов.
12.2) Что такое T в обобщениях?
Мы используем <T> для создания обобщенного класса, интерфейса и метода. T заменяется реальным типом при его использовании.
12.3) Как работают обобщения в Java?
Обобщенный код обеспечивает безопасность типов. Компилятор использует стирание типов для удаления всех параметров типов на этапе компиляции, чтобы уменьшить перегрузку во время выполнения.
13. Обобщения в Java – Дополнительные чтения
- Обобщения не поддерживают подтипизацию, поэтому
List<Number> numbers = new ArrayList<Integer>();
не будет компилироваться, узнайте почему обобщения не поддерживают подтипизацию. - Мы не можем создать обобщенный массив, поэтому
List<Integer>[] array = new ArrayList<Integer>[10]
не будет компилироваться, прочтите почему мы не можем создать обобщенный массив?.
Это все, что касается обобщений в Java, обобщенные типы данных в Java – это действительно обширная тема и требует много времени для понимания и эффективного использования. Этот пост – попытка предоставить основные сведения об обобщениях и как мы можем использовать их для расширения нашей программы с обеспечением типов безопасности.
Source:
https://www.digitalocean.com/community/tutorials/java-generics-example-method-class-interface