Generics — это новая функция JDK1.5. На самом деле это «синтаксический сахар», который по сути является небольшим «средством», предоставляемым компилятором для обеспечения лучшей читабельности. На уровне виртуальной машины так называемых «средств» нет. , концепция дженериков.
На мой взгляд, существование «дженериков» имеет следующие два значения, что также является первоначальным замыслом его дизайна.
Во-первых, с помощью определения синтаксиса дженериков компилятор может обеспечить определенные проверки безопасности типов во время компиляции, чтобы отфильтровать большинство исключений времени выполнения, вызванных несоответствиями типов, например:
ArrayList<Integer> list = new ArrayList<>();
list.add("ddddd"); //编译失败
Поскольку наш ArrayList является контейнером, который соответствует определению универсального синтаксиса, вы можете указать тип при создании экземпляра и ограничить контейнер только элементами типа Integer. И если вы принудительно вставите в него другие типы элементов, компилятор не пройдет.
Во-вторых, дженерики могут сделать программный код более читабельным, а поскольку это просто синтаксический сахар, он не влияет на производительность среды выполнения JVM.
Конечно, у «дженериков» есть и некоторые присущие недостатки.Хотя кажется, что они обеспечивают только функцию проверки безопасности типов, на самом деле реализация этого синтаксического сахара не так проста, как кажется, и она хорошо понятна. generics поможет вам разобраться в различных структурах коллекций контейнеров.
стирание типа
Понятие «стирание типа» вводится в начале, чтобы облегчить каждому первоначальное понимание «дженериков», чтобы было легче понять, как использовать последующие введения.
Обобщения — это синтаксический сахар, и компилятор «сотрет» общий синтаксис во время компиляции и соответственно выполнит некоторые действия по преобразованию типов. Например:
public class Caculate<T> {
private T num;
}
Мы определяем универсальный класс. Детали определения универсального класса будут подробно описаны позже. Здесь мы сосредоточимся на нашем процессе стирания типов. Атрибут-член определен. Тип члена является универсальным типом. Мы не знаем, что такое тип T. Он используется только для уточнения типа.
Конечно, мы также можем декомпилировать этот класс Caculate:
public class Caculate{
public Caculate(){}
private Object num;
}
В результате, по-видимому, компилятор стирает две угловые скобки после класса Caculate и определяет тип num как тип Object.
Конечно, можно спросить: «Все ли общие типы удаляются с помощью Object?»
Ответ таков: в большинстве случаев универсальные типы заменяются Object, но в одном случае это не так.
public class Caculate<T extends String> {
private T num;
}
Для универсальных типов в этом случае num заменяется на String вместо Object.
Это синтаксис с указанием типа, который ограничивает T как подкласс String или String, то есть, когда вы создаете экземпляр Caculate, вы можете ограничить T только как подкласс String или String, поэтому независимо от того, какой тип Если вы ограничиваете, String всегда является родительским классом, не будет проблем с несоответствием типов, поэтому String можно использовать для стирания типов.
Тогда у многих людей также будут такие вопросы: после того, как вы наберете стирание, возвращаемое значение всех методов, связанных с дженериками, будет Object, так что конкретный тип, ограниченный дженериками, все еще полезен? Например такой метод:
ArrayList<Integer> list = new ArrayList();
list.add(10);
Integer num = list.get(0);
//这是 ArrayList 内部的一个方法
public E get(int index) {
.....
}
Другими словами, после того, как вы введете стирание, возвращаемое значение E метода get будет стерто до типа Object, так почему же мы на самом деле видим возвращаемый тип Integer?
Это результат декомпиляции приведенных выше трех строк кода.Вы можете видеть, что компилятор фактически скомпилирует и напечатает ErrayList как обычно, а затем вернет экземпляр. Но кроме того, если общий синтаксис используется при построении экземпляра ArrayList, компилятор пометит экземпляр и обратит внимание на все последующие вызовы методов экземпляра, а перед каждым вызовом выполнит проверки безопасности, методы неуказанных типов не могут вызов успешно.
На самом деле есть еще один момент, на который, возможно, все обращают мало внимания: большинство людей просто знают, что компилятор напечатает стирание обобщенного класса и выполнит определенные проверки безопасности созданного экземпляра. Но на самом деле компилятор не только обращает внимание на вызов универсального метода, но и выполняет преобразование типов для некоторых методов, возвращаемое значение которых является квалифицированным универсальным типом. За исключением типа Object, при вызове этих методов компилятор ограничит результат только Integer или Object.
По сути, этот процесс мы называем «общим переводом». Я должен вздохнуть, компилятору требуется много усилий, чтобы обмануть виртуальную машину, чтобы предоставить программисту общие услуги.
Основное использование дженериков
Общие классы и интерфейсы
Определить универсальный класс или интерфейс несложно, давайте рассмотрим несколько универсальных классов в JDK.
- public class ArrayList
- public interface List
- public interface Queue
Основной формат таков:
访问修饰符 class/interface 类名或接口名<限定类型变量名>
"Квалифицированное имя переменной типа" может быть любым именем переменной, вы можете называть его T или E, если оно соответствует соглашению об именах переменных Java. Это эквивалентно объявлению универсального квалифицированного типа, и свойства или методы члена этого класса можно использовать напрямую.
общий метод
Здесь всем нужно уяснить, что обобщенный метод не обязательно зависит от своих внешних классов или интерфейсов, он может существовать независимо или зависеть от существования окружающих классов. Например:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
Метод get ArrayList является универсальным методом, который опирается на универсальный тип E, объявленный окружающим ArrayList, то есть он не объявляет универсальный тип сам по себе и использует окружающий класс.
Конечно, другой способ — объявить общий тип самостоятельно и использовать:
public class Caculate {
public <T> T add(T num){
return num;
}
}
Это еще одна форма универсального метода, где
Таким образом, внешние вызовы этого метода должны указывать квалифицированный тип для вызова, например:
Caculate caculate = new Caculate();
caculate.<Integer>add(12);
caculate.<String>add("fadf");
Целью использования дженериков является ограничение типа.Первоначально синтаксис дженериков не использовался, поэтому все параметры были типа Object.Теперь дженерики позволяют нам ограничивать конкретные типы, что должно быть понятно.
Конечно, вы могли не видеть такой синтаксис вызова.Пишете ли вы код каждый день или просматриваете реализацию исходного кода JDK, часть квалификации типа в основном опущена, то есть приведенный выше код эквивалентен следующему:
Caculate caculate = new Caculate();
caculate.add(12);
caculate.add("fadf");
Зачем? Поскольку компилятор выведет ваш тип параметра, вам разрешено опускать его, но предпосылка состоит в том, что ваш метод имеет параметры.Если логика вашего метода не требует передачи параметров, вам все равно нужно явно указать конкретный конкретный тип . Например:
public class Caculate {
public <T> T add(){
T num = null;
return num;
}
}
Caculate caculate = new Caculate();
caculate.add();
Такой вызов метода add означает, что вы не определили тип T, тогда этот T на самом деле имеет тип Object и не является квалифицированным.
квалификация общего типа
Квалификация типа здесь фактически относится к такому синтаксису:
<T extends String>
Его можно применять к определению универсального класса или интерфейса, а также к определению универсального метода.Он объявляет универсальный тип T, и тип T должен быть String или подклассом String, то есть внешним. конкретный квалифицированный тип, переданный при использовании, не может быть типом системы, отличной от String.
При использовании этого синтаксиса, поскольку компилятор гарантирует, что конкретный квалифицированный тип, переданный при внешнем использовании, не будет превышать String, Object больше не будет использоваться для стирания типа во время компиляции, а String может использоваться для стирания типа.
подстановочный знак
Подстановочные знаки — это специальный синтаксис, используемый для решения проблемы передачи по ссылке между дженериками. Посмотрите на следующий фрагмент кода:
public static void main(String[] args){
Integer[] integerArr = new Integer[2];
Number[] numberArr = new Number[2];
numberArr = integerArr;
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers;//编译不通过
}
В Java массив является ковариантным, то есть Integer расширяет число, тогда экземпляр массива подкласса может быть назначен экземпляру массива родительского класса. Это связано с тем, что тип массива в Java по существу генерирует тип динамически во время выполнения виртуальной машины.В дополнение к записи необходимых атрибутов массива, таких как длина, тип элемента и т. д., этот тип будет иметь указатель на определенное место в памяти, которая является начальной позицией этого элемента массива.
Следовательно, назначение экземпляра массива подкласса экземпляру массива родительского класса означает только то, что ссылка экземпляра массива родительского класса указывает на массив подкласса в куче, и конфликта не будет, поэтому Java разрешает эту операцию.
А дженерикам этого делать нельзя, почему?
Предположим, что дженерики допускают эту ковариацию, и посмотрим, что пойдет не так.
ArrayList<Integer> integers = new ArrayList<>();
ArrayList<Number> numbers = new ArrayList<>();
numbers = integers;//假设的前提下,编译器是能通过的
numbers.add(23.5);
Предполагая, что Java допускает общую ковариантность, приведенный выше код выглядит нормально для компилятора, но сталкивается с проблемами. Этот метод добавления фактически помещает число с плавающей запятой в целочисленный контейнер.Хотя стирание типа не вызовет проблем в работе программы, очевидно, что это нарушает исходный замысел дженериков и легко может вызвать логическую путаницу, поэтому Java просто запрещает универсальную ковариантность.
Таким образом, хотя ArrayList
Итак, если есть определенное требование, наш метод должен поддерживать как универсальный тип подкласса в качестве формального параметра, так и универсальный тип родительского класса в качестве формального параметра, что нам делать?
Мы используем подстановочные знаки для обработки таких потребностей, например:
public void test2(ArrayList<? extends Number> list){
}
ArrayList означает, что конкретный тип универсального типа неизвестен, но конкретный тип должен быть Number и его подклассами. Например: ArrayList
но,Подстановочные знаки часто используются в параметрах метода., не допускается в синтаксисе определения и вызова. Например, следующие операторы не поддерживаются:
ArrayList<?> list = new ArrayList<>();
Конечно, помимо подстановочного знака есть еще два типа:
- : подстановочный знак любого типа
- : должен быть родительским классом типа
Подстановочные знаки эквивалентны набору. Типы, соответствующие описанию подстановочного знака, помещаются в набор. Фактические параметры, передаваемые при вызове метода, должны быть членами этого набора, иначе компиляция завершится ошибкой.
Детали и ограничения
Подстановочный знак только для чтения
Рассмотрим этот фрагмент кода:
ArrayList<Number> list = new ArrayList<>();
ArrayList<?> arrayList = list;
arrayList.add(32);
arrayList.add("fadsf");
arrayList.add(new Object());
Вышеупомянутые три оператора добавления не могут быть скомпилированы. Это ограничение подстановочных знаков. Универсальные типы, соответствующие подстановочным знакам, можно только читать, но не записывать.
Причина тоже очень проста, ? означает неопределенный тип, то есть вы не знаете, какой тип данных вы помещаете в этот контейнер, поэтому вы можете только читать данные в нем, и вы не можете добавлять в него элементы слепо.
Дженерики не позволяют создавать массивы
Когда мы впервые представили подстановочные знаки, мы сказали, что массивы ковариантны, то есть экземпляры массива подкласса могут быть назначены экземплярам массива суперкласса. Мы также сказали, что универсальные типы не являются ковариантными, и даже если конкретные типы двух экземпляров универсальных классов являются отношениями родитель-потомок, они не могут быть преобразованы друг в друга.
В чем конкретная причина, которую мы также представили подробно, общий смысл заключается в том, что контейнер родительского класса может содержать элементы любого типа, а контейнер подкласса может содержать только элементы определенного типа, если родительский класс представляет контейнер подкласса, Тогда контейнер родительского класса может поместить в контейнер элементы, которые не разрешены текущим экземпляром подкласса, что вызовет логическую путаницу, поэтому Java этого не допускает.
Затем, если дженерикам разрешено создавать массивы, из-за ковариантности массивов универсальные массивы также должны иметь ковариантность, а сами дженерики не допускают ковариантности, что, естественно, конфликтует, поэтому универсальные массивы создавать нельзя.
Весь код, изображения, файлы в статье хранятся в облаке на моем GitHub:
(https://github.com/SingleYam/overview_java)
Добро пожаловать в официальную учетную запись WeChat: OneJavaCoder, все статьи будут синхронизированы в официальной учетной записи.