Что такое дженерики
Когда дело доходит до дженериков, все определенно незнакомы, в нашем коде много подобных утверждений:
List<String> list=new ArrayList<>();
ArrayList — это дженерик-класс, устанавливая разные типы, мы можем хранить в коллекции разные типы данных (и можно хранить только заданные типы данных, что является одним из преимуществ дженериков). «Универсальный» просто означает универсальный тип (параметризованный тип). Представьте себе такой сценарий: если мы сейчас хотим написать класс-контейнер (поддерживающий запросы на добавление и удаление данных), мы пишем класс, поддерживающий тип String, а затем нам нужно написать класс, поддерживающий тип Integer. Тогда что? Doubel, Float, различные пользовательские типы? Это слишком много повторяющегося кода, и все алгоритмы для этих контейнеров одинаковы. Мы можем заменить все типы, которые нам нужны раньше, ссылкой на тип T, и передать нужные нам типы в качестве параметров в контейнер, так что нашему алгоритму нужно написать только один набор для адаптации ко всем типам. Самый типичный пример — ArrayList, эта коллекция хорошо работает независимо от того, какой тип данных мы передаем.
Прочитав приведенное выше описание, у умных студентов возникла идея, и они написали следующий код:
class MyList{
private Object[] elements=new Object[10];
private int size;
public void add(Object item) {
elements[size++]=item;
}
public Object get(int index) {
return elements[index];
}
}
Этот код очень гибкий, и все типы могут быть преобразованы в класс Object, чтобы мы могли хранить в нем различные типы данных. Действительно, Java сделала это до дженериков. Но с этим есть проблема: если данных в коллекции много, то есть ошибка в каком-то преобразовании данных, которое нельзя найти во время компиляции. Но во время выполнения возникает исключение java.lang.ClassCastException. Например:
MyList myList=new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));
Мы храним несколько типов в этой коллекции (в некоторых случаях контейнер может хранить несколько типов данных) Если объем данных большой, во время преобразования неизбежно возникнут исключения, о которых нельзя узнать во время компиляции. С одной стороны, дженерики позволяют добавлять в коллекцию данные только одного типа, и в то же время позволяют находить эти ошибки во время компиляции, избегать исключений во время выполнения и повышать надежность кода.
Введение в дженерики Java
Давайте представим соответствующее содержание дженериков Java.Будут представлены следующие аспекты:
- Общий класс Java
- Общий метод Java
- Общий интерфейс Java
- Общее стирание Java и связанные с ним вещи
- Общие подстановочные знаки Java
Общий класс Java
Структура класса является самым основным элементом объектно-ориентированного подхода.Если наш класс должен иметь хорошую расширяемость, мы можем сделать его универсальным. Предположим, нам нужен класс-оболочка данных, который может хранить соответствующие типы данных, передавая различные типы данных. Давайте посмотрим на дизайн этого простого универсального класса:
class DataHolder<T>{
T item;
public void setData(T t) {
this.item=t;
}
public T getData() {
return this.item;
}
}
При определении универсального класса вам нужно только добавить параметры типа после имени класса.Конечно, вы также можете добавить несколько параметров, например
Наиболее распространенным вариантом использования универсальных классов является использование «кортежей». Мы знаем, что возвращаемое значение метода может возвращать только один объект. Если мы определяем универсальный класс и определяем 2 или даже 3 параметра типа, когда мы возвращаем объект, мы создаем такой «кортеж» данных и передаем несколько объектов через дженерики, так что у нас может быть несколько методов одновременно. .
Общий метод Java
Генераторы, которые мы представили ранее, работают со всем классом, теперь давайте представим дженерики. Универсальные методы могут существовать как в универсальных классах, так и в обычных классах. Если использование универсальных методов может решить проблему, вам следует попробовать использовать универсальные методы. Давайте рассмотрим использование универсальных методов на примере:
class DataHolder<T>{
T item;
public void setData(T t) {
this.item=t;
}
public T getData() {
return this.item;
}
/**
* 泛型方法
* @param e
*/
public <E> void PrinterInfo(E e) {
System.out.println(e);
}
}
Посмотрим на результаты работы:
1
AAAAA
8.88
Из приведенного выше примера мы видим, что мы определяем универсальный метод printInfo в универсальном классе. Передавая разные типы данных, мы все можем распечатать их. Внутри этого метода мы определяем параметр типа E. Между этим E и T в общем классе нет никакой связи. Даже если мы установим общий метод следующим образом:
//注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void PrinterInfo(T e) {
System.out.println(e);
}
//调用方法
DataHolder<String> dataHolder=new DataHolder<>();
dataHolder.PrinterInfo(1);
dataHolder.PrinterInfo("AAAAA");
dataHolder.PrinterInfo(8.88f);
Этот общий метод по-прежнему может передавать данные Double, Float и другие типы. Параметр типа T в универсальном методе и параметр типа в универсальном классе относятся к разным типам.Из приведенного выше вызывающего метода мы также можем видеть, что универсальный метод printInfo не зависит от параметра универсального типа в нашем DataHolder.Влияние строк . Подытожим некоторые основные характеристики универсальных методов:
- Между public и возвращаемым значением очень важно, что можно понимать как объявление этого метода как универсального.
- Только объявленный метод является универсальным методом, а метод-член, который использует универсальный тип в универсальном классе, не является универсальным методом.
- Указывает, что метод будет использовать универсальный тип T, только тогда универсальный тип T может использоваться в методе.
- Подобно определению универсального класса, здесь T может быть записан как любой идентификатор, а общие параметры, такие как T, E, K и V, часто используются для представления универсальных классов.
Общий интерфейс Java
Определение универсального интерфейса Java в основном такое же, как и определение универсального класса Java.Вот пример:
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
Здесь следует отметить две вещи:
- Когда универсальный интерфейс не передает универсальный аргумент, это то же самое, что и определение универсального класса.При объявлении класса необходимо добавить универсальное объявление в класс. Примеры следующие:
/* 即:class DataHolder implements Generator<T>{
* 如果不声明泛型,如:class DataHolder implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
- Если универсальный интерфейс передает параметр типа и реализует класс реализации универсального интерфейса, все места, где используются универсальные элементы, должны быть заменены переданным типом параметра. Примеры следующие:
class DataHolder implements Generator<String>{
@Override
public String next() {
return null;
}
}
Из этого примера видно, что все места, где реализуется T в классе, должны быть реализованы как String.
Общее стирание Java и связанные с ним вещи
Давайте посмотрим на пример ниже:
Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1); //class java.util.ArrayList
System.out.println(class2); //class java.util.ArrayList
System.out.println(class1.equals(class2)); //true
Глядя на вывод, мы обнаружили, что class1 и class2 на самом деле являются одним и тем же типом ArrayList, а переменные типа String и Integer, которые мы передали во время выполнения, были потеряны. Генераторы языка Java предназначены для совместимости с исходным старым кодом, а универсальный механизм Java использует механизм «стирания». Давайте рассмотрим более подробный пример:
class Table {}
class Room {}
class House<Q> {}
class Particle<POSITION, MOMENTUM> {}
//调用代码及输出
List<Table> tableList = new ArrayList<Table>();
Map<Room, Table> maps = new HashMap<Room, Table>();
House<Room> house = new House<Room>();
Particle<Long, Double> particle = new Particle<Long, Double>();
System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
/**
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*/
В приведенном выше коде мы хотим получить параметр типа класса во время выполнения, но видим, что возвращаются все «формальные параметры». Во время выполнения мы не получаем никакой объявленной информации о типе.
Уведомление:
Хотя компилятор удалит информацию о типе параметров в процессе компиляции, он обеспечит согласованность типов параметров внутри класса или метода.
Общий параметр будет стерт до первой границы (может быть несколько границ, повторно используйте ключевое слово extends, чтобы добавить границу к типу параметра). Компилятор фактически заменяет параметр типа типом его первой границы. Если границы не указаны, параметр типа будет стерт до Object. В следующем примере общий параметр T можно использовать как тип HasF.
public interface HasF {
void f();
}
public class Manipulator<T extends HasF> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
Информация о типе после ключевого слова extend определяет информацию, которую может сохранить универсальный параметр. Стирание типа Java удалит только типы HasF.
Принцип универсального стирания Java
Давайте посмотрим на это на примере, сначала посмотрим на неуниверсальную версию:
// SimpleHolder.java
public class SimpleHolder {
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String) holder.getObj();
}
}
// SimpleHolder.class
public class SimpleHolder {
public SimpleHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public java.lang.Object getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class SimpleHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
Ниже мы приводим общую версию с точки зрения байт-кода:
//GenericHolder.java
public class GenericHolder<T> {
T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
public static void main(String[] args) {
GenericHolder<String> holder = new GenericHolder<>();
holder.setObj("Item");
String s = holder.getObj();
}
}
//GenericHolder.class
public class GenericHolder<T> {
T obj;
public GenericHolder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public T getObj();
Code:
0: aload_0
1: getfield #2 // Field obj:Ljava/lang/Object;
4: areturn
public void setObj(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field obj:Ljava/lang/Object;
5: return
public static void main(java.lang.String[]);
Code:
0: new #3 // class GenericHolder
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Item
11: invokevirtual #6 // Method setObj:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #7 // Method getObj:()Ljava/lang/Object;
18: checkcast #8 // class java/lang/String
21: astore_2
22: return
}
Во время компиляции доступна информация о переменных типа. Следовательно, метод set может выполнять проверку типов в компиляторе, а недопустимые типы не могут пройти компиляцию. Но для метода get из-за механизма стирания фактическим типом ссылки во время выполнения является тип объекта. Чтобы «восстановить» тип возвращаемого результата, компилятор добавляет приведение типа после get. Таким образом, в строке 18 основного тела метода файла GenericHolder.class имеется логика преобразования типов. Он автоматически добавляется компилятором за нас.
Так что это делается для нас, когда объект универсального класса читается и записывается, добавляя ограничения в код.
Дефекты и способы устранения Java Generic Erasure
Универсальные типы нельзя явно использовать в операциях с типами среды выполнения, таких как приведение, instanceof и new. Потому что во время выполнения информация о типе всех параметров теряется. Код, подобный следующему, не компилируется:
public class Erased<T> {
private final int SIZE = 100;
public static void f(Object arg) {
//编译不通过
if (arg instanceof T) {
}
//编译不通过
T var = new T();
//编译不通过
T[] array = new T[SIZE];
//编译不通过
T[] array = (T) new Object[SIZE];
}
}
Итак, что мы можем сделать, чтобы исправить это? Вот несколько способов решить вышеуказанные проблемы один за другим.
Типовая проблема
Мы можем использовать следующий код для решения проблемы, связанной с тем, что информация о типе универсального типа не может быть оценена из-за стирания:
/**
* 泛型类型判断封装类
* @param <T>
*/
class GenericType<T>{
Class<?> classType;
public GenericType(Class<?> type) {
classType=type;
}
public boolean isInstance(Object object) {
return classType.isInstance(object);
}
}
В основном методе мы можем вызвать его так:
GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("------------");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));
Мы записываем объект класса параметра типа, а затем выполняем суждение о типе через этот объект класса.
Создать экземпляр типа
Есть две причины, по которым new T() нельзя использовать в универсальном коде: во-первых, невозможно определить тип из-за стирания, а во-вторых, невозможно определить, содержит ли T конструктор без параметров.
Чтобы избежать этих двух проблем, мы используем явный фабричный шаблон:
/**
* 使用工厂方法来创建实例
*
* @param <T>
*/
interface Factory<T>{
T create();
}
class Creater<T>{
T instance;
public <F extends Factory<T>> T newInstance(F f) {
instance=f.create();
return instance;
}
}
class IntegerFactory implements Factory<Integer>{
@Override
public Integer create() {
Integer integer=new Integer(9);
return integer;
}
}
Мы создаем объекты экземпляров через фабричный режим + универсальный метод. В приведенном выше коде мы создаем фабрику IntegerFactory для создания экземпляров Integer. Если код изменится в будущем, мы можем добавить новые типы фабрик.
Код вызова следующий:
Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));
Создайте общий массив
Создание универсальных массивов обычно не рекомендуется. Попробуйте использовать ArrayList вместо универсального массива. Но вот способ создать общий массив.
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T[] rep() {
return array;
}
public static void main(String[] args) {
}
}
Здесь мы по-прежнему используем тип параметра и используем метод newInstance типа для создания экземпляра.
Подстановочные знаки для Java Generics
Подстановочный знак верхней границы extends T>
Сначала рассмотрим пример:
class Fruit {}
class Apple extends Fruit {}
Теперь мы определяем подкласс пластины:
class Plate<T>{
T item;
public Plate(T t){
item=t;
}
public void set(T t) {
item=t;
}
public T get() {
return item;
}
}
Далее мы определяем фруктовую тарелку.Теоретически яблоки, конечно, могут существовать в фруктовой тарелке.
Plate<Fruit> p=new Plate<Apple>(new Apple());
Вы обнаружите, что этот код не компилируется. «Тарелка с яблоками» не может быть преобразована в «Тарелку с фруктами»:
cannot convert from Plate<Apple> to Plate<Fruit>
Из приведенного выше кода мы знаем, что даже при наличии отношения наследования между типами в контейнере отношения наследования между контейнером Plate и контейнером Plate отсутствуют. В этом случае Java разработан таким образом, чтобы Plate допускал наследование между двумя контейнерами. Вышеупомянутый код может быть назначен
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
Plate — это базовый класс для Plate
Давайте посмотрим на границы верхней границы на более подробном примере:
class Food{}
class Fruit extends Food {}
class Meat extends Food {}
class Apple extends Fruit {}
class Banana extends Fruit {}
class Pork extends Meat{}
class Beef extends Meat{}
class RedApple extends Apple {}
class GreenApple extends Apple {}
В приведенной выше иерархии классов Plate, покрывающий синюю часть ниже:
Если мы добавим данные в табличку, например:
p.set(new Fruit());
p.set(new Apple());
Вы обнаружите, что в нем нет способа установить данные Само собой разумеется, что мы установили общий тип в ?extend Fruit. Само собой разумеется, что у нас должна быть возможность добавить к нему подкласс Fruit. Но компилятор Java не позволяет этого. делает недействительным метод set(), который кладет что-то на тарелку. Но метод get() по-прежнему работает.
Причина в том, что:
Во время компиляции Java мы знаем только, что контейнер хранит Fruit и производные от него классы, я не знаю, что это за тип, может Fruit? Может Эппл? Может Банан, РедЭппл, ГринЭппл? После того, как компилятор позже увидит назначение Plate
Последнее понимание:
Ссылка на тарелку extends Fruit> может указывать на тарелку типа Тарелка, но, конечно же, на эту тарелку нельзя класть банан. Одно из моих понятий: тарелка extends Fruit> представляет собой тарелку, на которую можно положить только определенный тип фруктов, а не тарелку, на которую можно положить любые фрукты.
Но для операций чтения разрешены подстановочные знаки верхней границы. Пример кода:
Fruit fruit=p.get();
Object object=p.get();
Мы это очень хорошо понимаем.Поскольку контейнер настройки подстановочных знаков верхней границы может хранить только Fruit и его производные классы, мы можем неявно преобразовать полученные классы в их базовые классы (или базовые классы Object). Поэтому дескриптор верхней границы Extends подходит для часто читаемых сценариев.
Подстановочный знак Нижнего мира
Подстановочный знак нижней границы означает, что в контейнере могут храниться только данные типа T и его базового класса. Мы по-прежнему смотрим на вышеприведенный уровень класса, закрывает красную часть ниже:
Подстановочный знак нижней границы не влияет на хранение в нем, но считываемые данные могут быть только типа Object.Причина в том, что:
Подстановочный знак нижней границы указывает наименьшую степень детализации элемента, которым должен быть T и его базовые классы, поэтому я могу хранить в нем T и его производные классы, потому что его можно неявно преобразовать в тип T. Однако его трудно контролировать при считывании, он хранит T и его базовые классы, которые нельзя преобразовать ни в какой тип, можно загрузить только базовый класс Object.
принципы PECS
Наконец, краткое введение в принципы PECS, представленные в книге «Эффективная Java».
- Верхняя граница extends T> не может храниться внутри, а может быть получена только извне, что подходит для сценариев, в которых содержимое часто читается снаружи.
- Нижняя граница не влияет на хранение в, но извлечение может быть размещено только в объекте Object, что подходит для сценариев, когда в него часто вставляются данные.
> Неограниченное количество подстановочных знаков
Неограниченный подстановочный знак означает, что можно использовать любой объект, поэтому его использование похоже на использование примитивного типа. Но это работает, примитивный тип может содержать любой тип, а контейнер, модифицированный неограниченным подстановочным знаком, содержит определенный тип. Например, в ссылку типа List к ней нельзя добавить Object, а в ссылку типа List можно добавить переменную типа Object.
Последнее напоминание: List — это не то же самое, что List>, List — это подкласс List>. Кроме того, в список List> нельзя добавить какой-либо объект, кроме null.