<? extends T>
а также<? super T>
Это концепция «Подстановочных знаков» и «Границ» в дженериках Java.
- : означает «Подстановочные знаки верхних границ»
- : относится к «Подстановочным знакам нижних границ».
1. Зачем использовать подстановочные знаки и границы? -- Дженерики не являются ковариантными
Когда разработчики используют дженерики, легко сделать некоторые ошибки, основанные на их собственной интуиции. Например, если метод получает список в качестве формального параметра, то если вы попытаетесь пройти объект списка в качестве фактического параметра, вы обнаружите, что он не может быть скомпилирован.
Хотя от intite, объект - это родительский класс строки, который должен быть разумным. Но на самом деле это создаст проблему преобразования неявного типа, поэтому компилятор напрямую запрещено такого поведения.
Например, у нас естьFruitкласс и его производные классыApple
class Fruit {}
class Apple extends Fruit {}
Тогда есть самый простой контейнер:Plateкласс, можно поставить дженерик на тарелку"предмет"
Самое простое, что мы можем сделать с этой штукой"помещать"а также"Выбиратьдействие: **set()а такжеметод get( )**.
class Plate<T>{
private 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());
Но на самом деле компилятор Java не разрешает эту операцию. сообщит об ошибке"тарелка с яблоками"не может быть преобразовано в"тарелка с фруктами".
error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>
На самом деле логика, которую распознает компилятор, такова:
- яблокоIS-Aфрукты
- тарелка с яблокамиNOT-IS-Aтарелка с фруктами
* Следовательно, даже если между вещами в контейнере существует отношение наследования, между контейнерами нет отношения наследования. *
Поэтому мы не можемPlateссылка для переходаПластина.
Дженерики не являются ковариантными
В языке Java массивы являются ковариантными, то есть, если Integer расширяет число, не только целое число является числом, но целое число [] также является числом [], и целое число может быть передано или задано там, где требуется число [] []. (Более формально, если Number является супертипом Integer, то Number[] также является супертипом Integer[]).
Вы можете подумать, что тот же принцип применим к универсальным типам — List
стирание типа
Первая предпосылка для правильного понимания концепции дженериков — это понимание стирания типов. **** Универсальные типы в Java в основном реализуются на уровне компилятора. Информация о типе в дженериках не включается в сгенерированный байт-код Java. Параметры типа, добавленные при использовании дженериков, будут удалены компилятором во время компиляции. Этот процесс называется стиранием типа. Такие типы, как List и List, определенные в коде, после компиляции станут списком. **** JVM видит только список, а информация о типе, прикрепленная дженериками, невидима для JVM. Компилятор Java попытается найти возможные ошибки во время компиляции, но все же невозможно избежать исключений преобразования типов во время выполнения. Стирание типов также является общей реализацией Java иМеханизм шаблонов C++Важное различие между способами.
public class Test {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass().getName());
System.out.println(intList.getClass().getName());
}
}
Вышеуказанный код, выход следующий после запуска, видно, что информация типа, полученная во время выполнения, не имеет определенного типа:
java.util.ArrayList
java.util.ArrayList
Многие странные свойства дженериков связаны с существованием стирания этого типа, в том числе:
- Универсальный класс не имеет собственного уникального объекта класса. Например, нет List.class или List.class, а есть только List.class, поэтому информацию о реальном типе дженериков нельзя получить во время выполнения.
- Статические переменные являются общими для всех экземпляров универсального класса. Для класса, объявленного как MyClass, метод доступа к статическим переменным в нем по-прежнему MyClass.myStaticVar. Объекты, созданные либо новым MyClass, либо новым MyClass, совместно используют статическую переменную.
- Параметры универсального типа нельзя использовать в операторах catch при обработке исключений Java. Поскольку обработка исключений выполняется JVM во время выполнения. Поскольку информация о типе стирается, JVM не может различать два типа исключений MyException и MyException. Для JVM все они относятся к типу MyException. Кроме того, оператор catch, соответствующий исключению, не может быть выполнен.
** Основной процесс стирания шрифта также относительно прост, во-первых, нужно найти заменутип параметраконкретный класс. Этот конкретный класс обычно является Object. Если указана верхняя граница для параметра типа, используется эта верхняя граница. Замените все параметры типа в коде конкретными классами. При этом удалите появившееся объявление типа, то есть удалите содержимое . **Например, объявление метода T get() становится Object get(), а List становится List. Далее вам может понадобиться сгенерировать некоторые методы моста. Это связано с тем, что в классе после стирания типа могут отсутствовать некоторые необходимые методы. Например, рассмотрим следующий код:
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
Когда информация о типе стирается, приведенное выше объявление класса становится классом MyString, реализующим Comparable. Но в этом случае класс MyString будет иметь ошибку компиляции, потому что он не реализует метод int compareTo(Object), объявленный интерфейсом Comparable. В это время метод динамически генерируется компилятором.
Анализ случая
Как только вы поймете механизм стирания типов, вы поймете, что компилятор выполняет всю работу по проверке типов. **** Компилятор запрещает определенное использование дженериков именно для обеспечения безопасности типов. ****В качестве примера для конкретного анализа возьмем вышеупомянутый Список и Список:
public void inspect(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
list.add(1); // 这个操作在当前方法的上下文是合法的。
}
public void test() {
List<String> strs = new ArrayList<String>();
inspect(strs); // 编译错误
}
В этом коде метод inspect принимает список в качестве параметра, при попытке передать список в тестовом методе возникнет ошибка компиляции.
Предполагая, что это разрешено, метод проверки может вызвать list.add(1) для добавления числа в коллекцию. Таким образом, в методе испытанийОбъект типа Integer добавляется в коллекцию, объявленную как List. Это явно нарушает принцип безопасности типов и в какой-то момент обязательно выкинетClassCastException. Поэтому компилятор запрещает такое поведение.
Компилятор будет проверять возможные проблемы с безопасностью типов в максимально возможной степени. Ошибки компиляции указываются там, где установлено, что соответствующие принципы нарушены. Когда компилятор не может определить, правильно ли используется тип, он выдает предупреждающее сообщение.
Чтобы сделать дженерики более удобными в использовании, мастера Sun придумали и , чтобы сделать "фруктовая тарелка"а также"яблочная тарелка«Между ними существуют законные отношения.
подстановочный знак
При использовании универсальных классов
Можно указать любой конкретный тип, например, List объявляет, что конкретный тип — String;
Подстановочный знак ? также может использоваться для представления неизвестных типов, например, List> для объявления того, что тип элемента, содержащийся в списке, неизвестен.
Подстановочные знаки представляют набор типов, но конкретный тип неизвестен. Все, что List объявляет, это то, что разрешены все типы. Но список не эквивалентен списку.
Список фактически определяет этот список содержит объект и его подклассы, которые могут ссылаться через объект при использовании. В списке тип элементов, содержащихся в нем, является неопределенным. Это может содержать либо строку, либо целое число. Это ошибка, чтобы добавить элемент типа целого числа к нему, если он содержит строку. Поскольку тип неизвестен, новый объект ArrayList не может быть создан через новый метод ArrayList (). Потому что компилятор не имеет способа узнать, что такое конкретный тип. Но дляЭлементы в списке>Всегда можно использовать Object для ссылки, потому что, хотя тип неизвестен, это должен быть Object и его подклассы. Рассмотрим следующий код:
public void wildcard(List<?> list) {
list.add(1);// 编译错误
}
Как показано выше, при попытке работать с универсальным классом с подстановочным знаком всегда возникает ошибка компиляции. Причина этого в том, что тип, представленный подстановочным знаком, неизвестен.
ЭтоКраткое изложение общих подстановочных знаков JAVA (PECS) в трех предложенияхПервое предложение в: ?" не может добавлять элементы, только как потребители
Потому что на элементы в List> может ссылаться только Object, что в некоторых случаях не очень удобно. В этих случаях можно использовать верхнюю и нижнюю границы для ограничения диапазона неизвестных типов.Например, List указывает, что типы элементов, которые могут содержаться в List, — это Number и его подклассы. А List означает, что List содержит Number и его родительский класс. Когда вводится верхняя граница, методы, определенные в классе верхней границы, могут использоваться при использовании типа. Например, при доступе к List можно использовать такие методы, как intValue класса Number.
система типов
В Java мы больше знакомы с архитектурой типов через наследование. Например, String наследуется от Object. согласно сПринцип подстановки Лисков, подкласс может заменить суперкласс. Когда требуется ссылка на класс Object, нет проблем с передачей объекта String. Но в обратном случае, то есть когда ссылки на подклассы заменяются ссылками на суперклассы, требуются приведения типов. Компилятор не гарантирует, что такие преобразования допустимы во время выполнения. **Этот механизм автоматического преобразования типов подклассов, заменяющих суперклассы, также применим к массивам *(массивы ковариантны)*. String[] может заменить Object[]. Однако введение дженериков оказало определенное влияние на эту систему типов. **Как упоминалось ранее, List не может заменить List.
Система типов после введения дженериков добавила два измерения: первоетип параметрасвою собственную архитектуру наследования, другаяобщий классИли архитектура наследования самого интерфейса. Первый относится к случаю List и List, где параметр типа String наследуется от Object. Второй относится к интерфейсу List, наследуемому от интерфейса Collection. Для этой системы типов существуют следующие правила:
- Отношения универсальных классов с одним и тем же параметром типа зависят от архитектуры наследования самого универсального класса. То есть List является подтипом Collection, и List может заменить Collection. Это также относится к объявлениям типов с верхней и нижней границей.
- Когда в объявлении типа универсального класса используются подстановочные знаки, его подтипы могут раскрываться отдельно в двух измерениях. Например, для Collection его подтипы могут быть расширены на уровне Collection, а именно List и Set и т. д., а также могут быть расширены на уровне Number, а именно Сбор и сбор и т.д. В этом цикле ArrayList и HashSet также являются подтипами Collection.
- Если универсальный класс содержит несколько параметров типа, приведенные выше правила применяются отдельно для каждого параметра типа.
После понимания приведенных выше правил легко изменить код, приведенный в примере анализа. Просто измените List на List . Список является подтипом списка, поэтому при передаче параметров не возникает ошибок.
2. Верхняя граница
Ниже приведены подстановочные знаки верхних границ.
Plate<? extends Fruit>
Тарелка для фруктов и всего, что из фруктов
Говоря более прямо:Тарелка, которая может держать любые фрукты, что ближе к нашей человеческой логике
Тарелка Самая большая разница между extends Fruit> и Plate заключается в следующем: Plate extends Fruit> является базовым классом для Plate и Plate.
Непосредственным преимуществом является то, что мы можем использовать "яблочная тарелка"Дать"Поднос с фруктами"Назначенный.
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
Еще немного расширяется, еда разделена на фрукты и мясо, фрукты имеют яблоки и бананы, мясо имеет свинину и говядину, яблоки и два вида зеленых яблок и красные яблоки.
//Lev 1
class Food{}
//Lev 2
class Fruit extends Food{}
class Meat extends Food{}
//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}
//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}
В этой системе верхняя граница подстановочного знака Plate extends Fruit> Закрывает синюю область на изображении ниже.
3. Нижняя граница
Соответствующие подстановочные знаки Нижнего мира (нижние подстановочные знаки)
Plate<? super Fruit>
Он выражает противоположную концепцию:Тарелка для фруктов и всего, что на фруктовой основе.
Тарелка super Fruit> является базовым классом Plate, но не базовым классом Plate
В соответствии с примером только что, Plate super Fruit> Закрасьте область красным цветом на изображении ниже.
В-четвертых, побочные эффекты подстановочных знаков верхней и нижней границы
Границы упрощают преобразование между дженериками Java. Но не забывайте, что такие преобразования также имеют определенные побочные эффекты. То есть часть функционала контейнера может дать сбой.
Возьмем, к примеру, Тарелку. Мы можем сделать две вещи с тарелкой: установить( ) новые вещи в тарелку и получить( ) вещи с тарелки.
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
1. Верхнюю границу нельзя сохранить, а можно только вынуть
(1). сделает недействительным метод set() для помещения вещей на тарелку, но метод get() для взятия вещей по-прежнему действителен.
(2) Вынутые вещи можно хранить только во Fruit или его базовом классе и формировать вверх.
Например, два метода set() в следующем примере, вставляющие Apple и Fruit, сообщают об ошибках.
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
//不能存入任何元素
p.set(new Fruit()); //Error
p.set(new Apple()); //Error
//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get(); //Error
Компилятор знает, что контейнер является только Fruit или его производными классами, но не знает конкретно, какого типа и, следовательно, когда форма должна быть вынесена вверх из базового класса.
Может фрукты? Может Эппл? Может Банан, РедЭппл, ГринЭппл? После того, как компилятор увидит более позднее назначение с Plate, пластина не будет помечена как «яблоко». Вместо этого пометьте его заполнителем:capture#1, для представления захвата Fruit или подкласса Fruit, конкретный класс неизвестен, кодовое имя захват №1.
Тогда независимо от того, хотите ли вы вставить компилятор Apple, Meat или Fruit, вы не знаете, может ли он соответствовать этому захвату № 1, поэтому это не разрешено.
Таким образом, разница между подстановочными знаками > и параметрами типа заключается в том, что для компилятораВсе T представляют один и тот же тип.
Например, в универсальном методе ниже все три T относятся к одному и тому же типу, либо String, либо Integer...
public <T> List<T> fill(T... t);
Но подстановочные знаки не имеют этого ограничения, пластина просто означает:На тарелке что-то было, я не знаю, что это было.
2. Нижняя граница не влияет на хранение в, а выборку можно разместить только в объекте Object
(1) Использование нижней границы частично сделает недействительным метод get() для взятия вещей с тарелки и может быть сохранен только в объекте Object.
Поскольку указанная нижняя граница не ясна относительно верхней границы, ее можно поместить только в самый фундаментальный объект базового класса.
(2). Комнат () метод нормальный.
Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());
//存入元素正常
p.set(new Fruit());
p.set(new Apple());
//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get(); //Error
Fruit newFruit1=p.get(); //Error
Object newFruit2=p.get();
Поскольку нижняя граница устанавливает нижнюю границу минимальной детализации элемента, она фактически ослабляет контроль типа элемента-контейнера.
Поскольку элемент является базовым классом Fruit, он может храниться с меньшей степенью детализации, чем Fruit.
Но считывание элементов является трудоемким, и могут быть загружены только объекты Object базового класса всех классов. Но в этом случае вся информация о типе элемента теряется.
5. Принципы PECS
Наконец-то посмотреть, чтоПринцип PECS (Producer Extensions Consumer Super)Это уже очень хорошо понятно.
Продюсеры расширяют Продюсеры используют расширения, чтобы определить верхнюю границу и поместить в нее элементы для производства.
Потребитель Суперпотребители используют Супер, чтобы определить нижнюю границу и взять вещи для потребления.
1,Если содержимое часто считывается, целесообразно использовать верхнюю границу Extends, то есть можно использовать ограничение типа возвращаемого значения, для которого можно использовать extends, но не ограничение типа параметра.
2,Часто вставляется сзади, подходит для использования нижней границы SUPER, SUPER может использоваться для ограничений типа параметра и не может использоваться для возврата ограничений типа.
3.Подстановочные знаки, квалифицированные суперсупертипами, могут быть записаны в универсальные объекты, а подстановочные знаки, квалифицированные расширенными подтипами, могут быть прочитаны из универсальных объектов.
6. Отличные отзывы:
один:
Терминология опущена, цель состоит в том, чтобы сначала дать понять читателю.
Java — одиночное наследование, все унаследованные классы образуют дерево. Предположим, что A и B находятся в дереве наследования (иначе слова super и extend не имеют смысла). Супер B означает, что A является родителем или предком B, выше B. Расширение B означает, что A является подклассом или потомком B, ниже B.
Поскольку древовидная структура асимметрична сверху вниз, эти два выражения сильно различаются. Предположим, что есть два дженерика, записанные в определении функции как параметры функции (есть разница между формальными параметрами и фактическими параметрами):
-
Параметр написан как: t Super B>. Для этого универсального типа? Представляет собой тип элемента в контейнере. Поскольку только указано, что элемент должен быть суперклассом B, нет четкого и единого «корня» «Для элемента (за исключением неизбежного корня объекта), поэтому вы не можете на самом деле использовать этот универсальный тип, за исключением, за исключением элемента на объект. Поэтому для функции, которая записывает параметры в этой форме, в организме функции вы можете сделать это только с универсальным типом.операция вставки без чтения
-
Параметр записывается как: T, так как B указан как "корень" всех элементов, то можно смело использовать B для использования элементов в контейнере в любой момент, но есть проблема со вставкой, потому что B является дочерним элементом предка Существует много деревьев, и разные поддеревья несовместимы. Поскольку фактические параметры могут исходить из любого поддерева, ваша вставка, вероятно, уничтожит параметры функции. Поэтому для формальных параметров, записанных таким образом,Операция вставки запрещена, только чтение
два:
Во-первых, в целях безопасности при появлении дженериков все исключения, связанные с дженериками, должны быть найдены при компиляции, поэтому для абсолютной безопасности дженериков в java сделаны соответствующие ограничения в дизайне:
List extends E> означает, что все подтипы E (включая сам E) хранятся в коллекции списка.Поскольку может быть много подтипов E, мы можем фактически хранить только один из подтипов при сохранении элементов (это для безопасность дженериков, потому что при компиляции будет сгенерирован метод моста (в этом методе будет приведение, а при наличии нескольких подтипов приведение не будет выполнено), например:
List<? extends Number> list=new ArrayList<Number>();
list.add(4.0);//编译错误
list.add(3);//编译错误
В приведенном выше примере добавлено более одного типа элемента, поэтому приведение компилятором не удастся.В целях безопасности Java может спроектировать его только таким образом, чтобы никакие элементы не могли быть добавлены.
Хотя List не может добавлять элементы, а потому, что все элементы в нем имеют одну общую черту — у них есть общий родительский класс, поэтому мы можем единообразно привести их к типу E, когда получим элементы, которые мы называем Этополучить принцип.
Для List все элементы супертипа E (включая E) хранятся в списке. Когда мы добавляем в него элементы, мы можем добавлять в него только элементы подтипа E (включая тип E) , чтобы в It было типобезопасно, когда оно приводится к типу E во время компиляции, чтобы можно было добавлять элементы, например:
List<? super Number> list=new ArrayList<Number>();
list.add(2.0);
list.add(3.0);
Однако, поскольку элементы в этом наборе являются всеми родительскими типами E (включая E), а существует много типов элементов, мы не можем определить, какой тип является, когда мы получаем элементы, поэтому он разработан таким образом, что мы не можем получить элементы, что мы называем этопут**** принцип.
На самом деле цель использования extensions и super для расширения дженериков состоит в том, чтобы восполнить недостаток, заключающийся в том, что List может хранить только определенный тип данных, и расширить его до List, чтобы он мог получать подтипы E. Любой тип элемента, что делает его более широко используемым.
То же верно и для List.
Использованная литература:
Глубокое понимание Java Generics
Глубокое приключение Java Generics
Как понять super в Java generics ? Чем он отличается от расширений?
утверждение
автор:yaoshengting
**Источник: **Авторское право CSDN принадлежит автору
Цель: к предыдущей статье —От небольших знаний до раскрытия продвинутого синтаксиса Java — дженерикиДополнение к знанию подстановочных знаков в
Эта статья воспроизведена из—blog.CSDN.net/YST собирается…