Ответы:
tl; dr: «PECS» с точки зрения коллекции. Если вы берете только предметы из общей коллекции, это производитель, и вы должны использовать его extends
; если вы только набиваете предметы, это потребитель, и вы должны его использовать super
. Если вы делаете оба с одной коллекцией, вы не должны использовать либо extends
или super
.
Предположим, у вас есть метод, который принимает в качестве параметра набор вещей, но вы хотите, чтобы он был более гибким, чем просто принятие a Collection<Thing>
.
Случай 1: Вы хотите просмотреть коллекцию и сделать что-то с каждым предметом.
Тогда список является производителем , поэтому вы должны использовать Collection<? extends Thing>
.
Причина заключается в том, что a Collection<? extends Thing>
может содержать любой подтип Thing
, и поэтому каждый элемент будет вести себя как a, Thing
когда вы выполняете свою операцию. (На самом деле вы ничего не можете добавить к a Collection<? extends Thing>
, потому что вы не можете знать во время выполнения, какой конкретный подтип Thing
коллекции содержит.)
Случай 2: Вы хотите добавить вещи в коллекцию.
Тогда список является потребителем , поэтому вы должны использовать Collection<? super Thing>
.
Причиной здесь является то Collection<? extends Thing>
, что в отличие от этого , Collection<? super Thing>
всегда может храниться Thing
независимо от того, что является фактическим параметризованным типом. Здесь вас не волнует, что уже есть в списке, если это позволит добавить a Thing
; это то, что ? super Thing
гарантирует.
doSomethingWithList(List list)
, вы потребляя список и поэтому нужно будешь ковариация / продолжается (или инвариантное List). С другой стороны , если ваш метод List doSomethingProvidingList
, то вы производить этот список и будет нуждаться в контравариации / супер (или инвариантное List).
const
параметры метода в C ++ для обозначения того, что метод не изменяет аргументы?
Принципы, стоящие за этим в информатике называется
? extends MyClass
,? super MyClass
иMyClass
Картинка ниже должна объяснить концепцию. Фото предоставлено Андреем Тюкиным
PECS (Производитель extends
и Потребитель super
)
мнемоника → принцип «получи и положи».
Этот принцип гласит, что:
Пример на Java:
class Super {
Object testCoVariance(){ return null;} //Covariance of return types in the subtype.
void testContraVariance(Object parameter){} // Contravariance of method arguments in the subtype.
}
class Sub extends Super {
@Override
String testCoVariance(){ return null;} //compiles successfully i.e. return type is don't care(String is subtype of Object)
@Override
void testContraVariance(String parameter){} //doesn't support even though String is subtype of Object
}
Принцип подстановки Лискова: если S является подтипом T, то объекты типа T могут быть заменены объектами типа S.
В системе типов языка программирования, правило ввода
Ковариация и контравариантность
Чтобы проиллюстрировать это общее явление, рассмотрим тип массива. Для типа Animal мы можем сделать тип Animal []
Примеры Java:
Object name= new String("prem"); //works
List<Number> numbers = new ArrayList<Integer>();//gets compile time error
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //attempt of heap pollution i.e. at runtime gets java.lang.ArrayStoreException: java.lang.Double(we can fool compiler but not run-time)
List<String> list=new ArrayList<>();
list.add("prem");
List<Object> listObject=list; //Type mismatch: cannot convert from List<String> to List<Object> at Compiletime
ограниченный (т.е. направленный куда-то) подстановочный знак : есть 3 различных вида подстановочных знаков:
?
или ? extends Object
- Неограниченный подстановочный знак. Это обозначает семью всех типов. Используйте, когда вы оба получите и положите.? extends T
(семейство всех типов, являющихся подтипами T
) - подстановочный знак с верхней границей . T
самый верхний класс в иерархии наследования. Используйте extends
подстановочный знак, когда вы только получаете значения из структуры.? super T
(семейство всех типов, являющихся супертипами T
) - подстановочный знак с нижней границей . T
самый нижний класс в иерархии наследования. Используйте super
подстановочный знак, когда вы только помещаете значения в структуру.Примечание: подстановочный знак ?
означает ноль или один раз , представляет неизвестный тип. Подстановочный знак может использоваться как тип параметра, никогда не использоваться в качестве аргумента типа для вызова универсального метода, создания экземпляра универсального класса (т. Е. Когда используется подстановочный знак, ссылка на который не используется в других частях программы, как мы используем T
)
class Shape { void draw() {}}
class Circle extends Shape {void draw() {}}
class Square extends Shape {void draw() {}}
class Rectangle extends Shape {void draw() {}}
public class Test {
/*
* Example for an upper bound wildcard (Get values i.e Producer `extends`)
*
* */
public void testCoVariance(List<? extends Shape> list) {
list.add(new Shape()); // Error: is not applicable for the arguments (Shape) i.e. inheritance is not supporting
list.add(new Circle()); // Error: is not applicable for the arguments (Circle) i.e. inheritance is not supporting
list.add(new Square()); // Error: is not applicable for the arguments (Square) i.e. inheritance is not supporting
list.add(new Rectangle()); // Error: is not applicable for the arguments (Rectangle) i.e. inheritance is not supporting
Shape shape= list.get(0);//compiles so list act as produces only
/*You can't add a Shape,Circle,Square,Rectangle to a List<? extends Shape>
* You can get an object and know that it will be an Shape
*/
}
/*
* Example for a lower bound wildcard (Put values i.e Consumer`super`)
* */
public void testContraVariance(List<? super Shape> list) {
list.add(new Shape());//compiles i.e. inheritance is supporting
list.add(new Circle());//compiles i.e. inheritance is supporting
list.add(new Square());//compiles i.e. inheritance is supporting
list.add(new Rectangle());//compiles i.e. inheritance is supporting
Shape shape= list.get(0); // Error: Type mismatch, so list acts only as consumer
Object object= list.get(0); // gets an object, but we don't know what kind of Object it is.
/*You can add a Shape,Circle,Square,Rectangle to a List<? super Shape>
* You can't get an Shape(but can get Object) and don't know what kind of Shape it is.
*/
}
}
In-variance/Non-variance: ? or ? extends Object - Unbounded Wildcard. It stands for the family of all types. Use when you both get and put.
я не могу добавить элемент в список <?> Или список <? расширяет объект>, поэтому я не понимаю, почему это может быть Use when you both get and put
.
?
- «неограниченный подстановочный знак» - соответствует полной противоположности инвариантности. Пожалуйста, обратитесь к следующей документации: docs.oracle.com/javase/tutorial/java/generics/…, которая гласит: В случае, когда коду требуется доступ к переменной как к переменным «in», так и к «out», выполните не используйте подстановочный знак. (Они используют «in» и «out» как синонимы «get» и «put»). За исключением того, что null
вы не можете добавить в коллекцию с параметром ?
.
public class Test {
public class A {}
public class B extends A {}
public class C extends B {}
public void testCoVariance(List<? extends B> myBlist) {
B b = new B();
C c = new C();
myBlist.add(b); // does not compile
myBlist.add(c); // does not compile
A a = myBlist.get(0);
}
public void testContraVariance(List<? super B> myBlist) {
B b = new B();
C c = new C();
myBlist.add(b);
myBlist.add(c);
A a = myBlist.get(0); // does not compile
}
}
? extends B
означает B и все, что расширяет B.
Как я объясняю в своем ответе на другой вопрос, PECS - это мнемоническое устройство, созданное Джошем Блохом, чтобы помочь вспомнить P roducer extends
, C onsumer super
.
Это означает, что когда параметризованный тип, передаваемый методу, будет генерировать экземпляры
T
(они будут каким-то образом извлечены из него), его? extends T
следует использовать, поскольку любой экземпляр подклассаT
также является aT
.Когда параметризованный тип, передаваемый методу, будет потреблять экземпляры
T
(они будут переданы ему для выполнения чего-либо), его? super T
следует использовать, поскольку экземплярT
можно легально передать любому методу, который принимает некоторый супертипT
. Например,Comparator<Number>
можно использовать наCollection<Integer>
.? extends T
не будет работать, потому чтоComparator<Integer>
не может работать наCollection<Number>
.
Обратите внимание , что , как правило , вы должны быть только с помощью ? extends T
и ? super T
для параметров некоторого метода. Методы должны просто использоваться T
в качестве параметра типа общего возвращаемого типа.
Короче говоря, три простых правила, чтобы запомнить PECS:
<? extends T>
подстановочный знак, если вам нужно получить объект типа T
из коллекции.<? super T>
подстановочный знак, если вам нужно поместить объекты типа T
в коллекцию.давайте предположим эту иерархию:
class Creature{}// X
class Animal extends Creature{}// Y
class Fish extends Animal{}// Z
class Shark extends Fish{}// A
class HammerSkark extends Shark{}// B
class DeadHammerShark extends HammerSkark{}// C
Давайте уточним PE - Производитель расширяет:
List<? extends Shark> sharks = new ArrayList<>();
Почему вы не можете добавить объекты, которые расширяют "Акула" в этом списке? подобно:
sharks.add(new HammerShark());//will result in compilation error
Поскольку у вас есть список, который может быть типа A, B или C во время выполнения , вы не можете добавить в него любой объект типа A, B или C, потому что вы можете получить комбинацию, которая не разрешена в Java.
На практике компилятор действительно может видеть во время компиляции, что вы добавляете B:
sharks.add(new HammerShark());
... но он не может определить, будет ли во время выполнения ваш B подтипом или супертипом типа списка. Во время выполнения тип списка может быть любым из типов A, B, C. Таким образом, вы не можете добавить HammerSkark (супертип) в список, например, DeadHammerShark.
* Вы скажете: «Хорошо, но почему я не могу добавить в него HammerSkark, так как это самый маленький тип?». Ответ: это самое маленькое, что вы знаете. Но HammerSkark может быть расширен кем-то другим, и вы в конечном итоге в том же сценарии.
Давайте поясним CS - Consumer Super:
В той же иерархии мы можем попробовать это:
List<? super Shark> sharks = new ArrayList<>();
Что и почему вы можете добавить в этот список?
sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());
Вы можете добавить вышеупомянутые типы объектов, потому что все, что находится ниже акулы (A, B, C), всегда будет подтипом чего-либо, что находится выше акулы (X, Y, Z). Легко понять.
Вы не можете добавлять типы выше Shark, потому что во время выполнения тип добавленного объекта может быть выше в иерархии, чем объявленный тип списка (X, Y, Z). Это не разрешено
Но почему вы не можете прочитать из этого списка? (Я имею в виду, что вы можете извлечь из него элемент, но вы не можете назначить его чему-либо, кроме Object o):
Object o;
o = sharks.get(2);// only assignment that works
Animal s;
s = sharks.get(2);//doen't work
Во время выполнения тип списка может быть любого типа выше A: X, Y, Z, ... Компилятор может скомпилировать ваш оператор присваивания (который кажется правильным), но во время выполнения тип s (Animal) может быть ниже в иерархия, чем объявленный тип списка (который может быть Существо или выше). Это не разрешено
Подводить итоги
Мы используем <? super T>
для добавления объектов типов, равных или ниже T
к List
. Мы не можем читать из этого.
Мы используем <? extends T>
для чтения объектов типов, равных или ниже T
из списка. Мы не можем добавить элемент к нему.
(добавив ответ, потому что примеров с подстановочными знаками Generics недостаточно)
// Source
List<Integer> intList = Arrays.asList(1,2,3);
List<Double> doubleList = Arrays.asList(2.78,3.14);
List<Number> numList = Arrays.asList(1,2,2.78,3.14,5);
// Destination
List<Integer> intList2 = new ArrayList<>();
List<Double> doublesList2 = new ArrayList<>();
List<Number> numList2 = new ArrayList<>();
// Works
copyElements1(intList,intList2); // from int to int
copyElements1(doubleList,doublesList2); // from double to double
static <T> void copyElements1(Collection<T> src, Collection<T> dest) {
for(T n : src){
dest.add(n);
}
}
// Let's try to copy intList to its supertype
copyElements1(intList,numList2); // error, method signature just says "T"
// and here the compiler is given
// two types: Integer and Number,
// so which one shall it be?
// PECS to the rescue!
copyElements2(intList,numList2); // possible
// copy Integer (? extends T) to its supertype (Number is super of Integer)
private static <T> void copyElements2(Collection<? extends T> src,
Collection<? super T> dest) {
for(T n : src){
dest.add(n);
}
}
Это самый ясный и простой способ для меня думать о расширяется против супер:
extends
для чтения
super
для написания
Я считаю, что «PECS» - это неочевидный способ думать о том, кто является «производителем», а кто «потребителем». «PECS» определяется с точки зрения самого сбора данных - коллекция «потребляет», если в нее записываются объекты (она потребляет объекты из вызывающего кода), и «выдает», если объекты читаются из нее (это создает объекты для некоторого вызывающего кода). Это противоречит тому, как все остальное называется. Стандартные API Java названы с точки зрения вызывающего кода, а не самой коллекции. Например, представление java.util.List, ориентированное на коллекцию, должно иметь метод с именем «receive ()» вместо «add ()» - в конце концов,элемент, но сам список получает элемент.
Я думаю, что более интуитивно, естественно и непротиворечиво думать о вещах с точки зрения кода, который взаимодействует с коллекцией - код «читает из» или «записывает» в коллекцию? После этого любой код, записывающий в коллекцию, будет «производителем», а любой код, читающий из коллекции, будет «потребителем».
src
и dst
. Таким образом, вы имеете дело как с кодом, так и с контейнерами одновременно, и я в конечном итоге задумался над этим следующим образом: «потребляющий код» потребляет из производящего контейнера, а «производящий код» производит для потребляющего контейнера.
«Правило» PECS гарантирует, что следующее является законным:
?
это ни было , это может юридически относиться к T
?
есть, это может быть законно ссылаются T
Типичное спаривание по типу List<? extends T> producer, List<? super T> consumer
просто гарантирует, что компилятор может применять стандартные правила отношений наследования "IS-A". Если бы мы могли сделать это легально, было бы проще сказать <T extends ?>, <? extends T>
(или еще лучше в Scala, как вы можете видеть выше, это так [-T], [+T]
. К сожалению, лучшее, что мы можем сделать, это <? super T>, <? extends T>
.
Когда я впервые столкнулся с этим и сломал его в своей голове, механика имела смысл, но сам код продолжал казаться мне запутанным - я продолжал думать, что «кажется, что границы не должны быть так перевернуты» - хотя я По вышесказанному было ясно - что речь идет просто о гарантии соблюдения стандартных правил ведения.
Что помогло мне, так это обычное задание в качестве аналогии.
Рассмотрим следующий (не готовый к производству) игрушечный код:
// copies the elements of 'producer' into 'consumer'
static <T> void copy(List<? extends T> producer, List<? super T> consumer) {
for(T t : producer)
consumer.add(t);
}
Иллюстрируя это с точки зрения аналогии назначения, для consumer
в ?
подстановки (неизвестный тип) ссылка - «левая сторона» на уступки - и <? super T>
гарантирует , что все ?
это, T
«IS-A» ?
- что T
может быть возложены на него, потому что ?
это супер тип (или не более того же типа), как T
.
Для producer
концерна это то же самое , это просто перевернутая: producer
«s ?
подстановочные (неизвестный тип) является референтом - в„правую“выполнения задания - и <? extends T>
гарантирует , что все ?
это, ?
„IS-A“ T
- что он может быть назначен на аT
, потому что ?
это подтип (или, по крайней мере, тот же тип), что и T
.
Используя пример из реальной жизни (с некоторыми упрощениями):
<? super FreightCarSize>
<? extends DepotSize>
Ковариантность : принять подтипы.
Контравариантность : принять подтипы .
Ковариантные типы доступны только для чтения, а контравариантные - только для записи.
Давайте посмотрим на пример
public class A { }
//B is A
public class B extends A { }
//C is A
public class C extends A { }
Обобщения позволяют безопасно и динамически работать с типами.
//ListA
List<A> listA = new ArrayList<A>();
//add
listA.add(new A());
listA.add(new B());
listA.add(new C());
//get
A a0 = listA.get(0);
A a1 = listA.get(1);
A a2 = listA.get(2);
//ListB
List<B> listB = new ArrayList<B>();
//add
listB.add(new B());
//get
B b0 = listB.get(0);
Поскольку коллекция Java является ссылочным типом, в результате мы имеем следующие проблемы:
Проблема № 1
//not compiled
//danger of **adding** non-B objects using listA reference
listA = listB;
* У универсала Swift такой проблемы нет, потому что Collection - это Value type
[About], поэтому создается новая коллекция.
Проблема № 2
//not compiled
//danger of **getting** non-B objects using listB reference
listB = listA;
Подстановочный знак является признаком ссылочного типа, и его нельзя создать напрямую
Решение # 1, также
<? super A>
известное как нижняя граница, также противоречащее потребителям, гарантирует, что оно управляется A и всеми суперклассами, поэтому его можно безопасно добавлять.
List<? super A> listSuperA;
listSuperA = listA;
listSuperA = new ArrayList<Object>();
//add
listSuperA.add(new A());
listSuperA.add(new B());
//get
Object o0 = listSuperA.get(0);
Решение № 2
<? extends A>
ака верхняя граница ака ковариантность ака производителей гарантирует, что он работает по A и всем подклассам, поэтому его безопасно получать и разыгрывать
List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;
//get
A a0 = listExtendsA.get(0);
super
часть, но дает представление о другой.