我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。
有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?
我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。
有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?
当前回答
正如我在回答另一个问题时所解释的,PECS是乔什·布洛克(Josh Bloch)为帮助记住Producer extends和Consumer super而创建的助记符设备。
这意味着当传递给方法的参数化类型将生成T的实例(它们将以某种方式从中检索)时?应该使用extends T,因为T的子类的任何实例也是T。当传递给方法的参数化类型将消耗T的实例(它们将传递给它以执行某些操作)时?应该使用super T,因为T的实例可以合法地传递给任何接受某个T超类型的方法。例如,可以在Collection<Integer>上使用Comparator<Number>?extends T无法工作,因为Comparator<Integer>无法对Collection<Number>进行操作。
请注意,通常您只应使用?延伸T和?super T表示某些方法的参数。方法应该只使用T作为泛型返回类型的类型参数。
其他回答
tl;博士:“PECS”是从藏品的角度来看的。如果您只是从泛型集合中提取项,那么它是一个生产者,您应该使用extends;如果你只是在填充物品,那么它是一个消费者,你应该使用super。如果同时使用同一集合,则不应使用extends或super。
假设您有一个方法,它以一个集合作为参数,但您希望它比只接受集合<Thing>更灵活。
案例1:你想通过收集并处理每个项目。那么列表是一个生产者,因此您应该使用集合<?扩展Thing>。
理由是集合<?extendsThing>可以保存Thing的任何子类型,因此当您执行操作时,每个元素都将表现为Thing。(实际上不能向集合中添加任何内容(null除外)<?扩展Thing>,因为您无法在运行时知道集合持有Thing的哪个特定子类型。)
案例2:您想要向集合中添加内容。那么列表是消费者,因此您应该使用集合<?超级事物>。
这里的理由是,不同于集合<?扩展Thing>,Collection<?super Thing>可以始终保存Thing,无论实际参数化类型是什么。在这里,只要它允许添加Thing,您就不在乎列表中已经存在什么;这是什么?super Thing保证。
PECS“规则”仅确保以下内容合法:
消费者:什么?它可以合法地指代T制片人:什么?是的,它可以在法律上被T
列表中的典型配对<?扩展T>生产者,列表<?super T>使用者只是确保编译器可以强制执行标准的“is-A”继承关系规则。如果我们可以合法地这样做,那么说<T extends?>、<?extended T>(或者在Scala中更好,如您所见,它是[-T],[+T]。不幸的是,我们能做的最好的是<?super T>,<?extended 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);
}
用任务类比来说明这一点,对于消费者来说?通配符(未知类型)是引用-赋值的“左侧”-和<?超级T>确保了什么?T“is-A”?-T可以分配给它,因为?是T的超类型(或最多相同类型)。
对于制片人来说,他们的担忧是一样的,只是颠倒了:制片人的?通配符(未知类型)是指代-赋值的“右手边”-和<?扩展T>确保了什么?是“IS-A”T-它可以分配给T,因为?是T的子类型(或至少相同类型)。
这是我认为extends与super最清晰、最简单的方式:
扩展用于读取super是用来写作的
我发现“PECS”是一种不明显的方式来思考谁是“生产者”,谁是“消费者”。“PECS”是从数据集合本身的角度定义的——如果对象正在被写入到集合中,则集合“消耗”(它消耗来自调用代码的对象),如果对象正在从集合中读取,则它“产生”(它向某些调用代码产生对象)。这与其他所有事物的命名方式相反。标准JavaAPI是从调用代码的角度命名的,而不是从集合本身命名的。例如,java.util.List的以集合为中心的视图应该有一个名为“receive()”的方法,而不是“add()”——毕竟,调用代码添加了元素,但列表本身接收了元素。
我认为从与集合交互的代码的角度来思考事情更直观、更自然、更一致——代码是“从集合中读取”还是“写入”集合?之后,向集合写入的任何代码都将是“生产者”,从集合读取的任何代码将是“消费者”。
PECS:生产者延伸和消费者超级
理解的前提条件:
泛型和泛型通配符多态性、亚型和超型
假设我们有一个采用泛型类型参数T的类型,例如List<T>。当我们编写代码时,还允许泛型类型参数T的子类型或超类型可能是有益的。这放松了对API用户的限制,可以使代码更加灵活。
让我们先看看放松这些限制会带来什么。假设我们有以下3个类:
class BaseAnimal{};
class Animal extends BaseAnimal{};
class Duck extends Animal{};
我们正在构建一个公共方法,该方法采用列表<Animal>
如果我们使用超级列表<?superAnimal>而不是List<Animal>,我们现在可以传递更多的列表来满足我们方法的要求。我们现在可以传入List<Animal>或List<BaseAnimal>甚至List<Object>如果我们使用扩展列表<?扩展Animal>而不是List<Animal>,我们现在可以传递更多的列表来满足我们方法的要求。我们现在可以传入List<Animal>或List<Duck>
然而,这存在以下两个限制:
如果我们使用像List<?super Animal>我们不知道List<t>的确切类型。它可能是List<Animal>或List<BaseAnimal>或List<Object>的列表。我们无从得知。这意味着我们永远无法从列表中获取值,因为我们不知道该类型是什么。但是,我们可以将任何Animal数据类型或将其扩展到列表中。因为我们只能将数据放入列表,所以它被称为数据消费者。如果我们使用扩展列表<?扩展Animal>而不是List<Animal>。我们也不知道确切的类型是什么。它可以是List<Animal>或List<Duck>。我们现在不能在列表中添加一些东西,因为我们永远无法确定是什么类型。但是我们可以删除一些东西,原因是我们始终知道列表中的任何东西都是Animal的子类型。因为我们只能从列表中提取数据,所以它被称为数据的生产者。
这里有一个简单的程序来说明类型限制的放松:
import java.util.ArrayList;
import java.util.List;
public class Generics {
public static void main(String[] args) {
Generics generics = new Generics();
generics.producerExtends(new ArrayList<Duck>());
generics.producerExtends(new ArrayList<Animal>());
generics.consumerSuper(new ArrayList<Object>());
generics.consumerSuper(new ArrayList<Animal>());
}
// ? extends T is an upper bound
public void producerExtends (List<? extends Animal> list) {
// Following are illegal since we never know exactly what type the list will be
// list.add(new Duck());
// list.add(new Animal());
// We can read from it since we are always getting an Animal or subclass from it
// However we can read them as an animal type, so this compiles fine
if (list.size() > 0) {
Animal animal = list.get(0);
}
}
// ? extends T is a lower bound
public void consumerSuper (List<? super Animal> list) {
// It will be either a list of Animal or a superclass of it
// Therefore we can add any type which extends animals
list.add(new Duck());
list.add(new Animal());
// Compiler won't allow this it could potentially be a super type of Animal
// Animal animal = list.get(0);
}
在处理集合时,在上限或下限通配符之间进行选择的常见规则是PECS。信用
PECS(生产者延伸和消费者超级)
助记符→ 获取(扩展)和放置(超级)原则。
该原则规定:仅从结构中获取值时,请使用扩展通配符。仅将值放入结构中时,请使用超级通配符。当你得到和得到时,不要使用通配符。
Java示例:
class Super {
Number testCoVariance() {
return null;
}
void testContraVariance(Number parameter) {
}
}
class Sub extends Super {
@Override
Integer testCoVariance() {
return null;
} //compiles successfully i.e. return type is don't care(Integer is subtype of Number)
@Override
void testContraVariance(Integer parameter) {
} //doesn't support even though Integer is subtype of Number
}
Liskov替换原则(LSP)指出,“程序中的对象应该用其子类型的实例替换,而不改变程序的正确性”。
在编程语言的类型系统中,键入规则
协变的,如果它保持类型的排序(≤),则将类型从更具体到更通用排序;相反,如果它颠倒了这个顺序;如果两者都不适用,则为不变或非变。
协方差和反方差
只读数据类型(源)可以是协变的;只写数据类型(汇)可以是相反的。充当源和汇的可变数据类型应该是不变的。
要说明这种一般现象,请考虑数组类型。对于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
更多示例
图像src
有界(即朝向某处)通配符:有三种不同的通配符:
差异/非差异:?或扩展对象-无边界通配符。它代表所有类型的家庭。当你得到和投入时使用。共方差:?扩展T(T子体的统治)-带有上限的通配符。T是继承层次结构中最上层的类。仅从结构中获取值时,请使用扩展通配符。抵销方差:?超级T(T祖先的统治)-带有下限的通配符。T是继承层次结构中最底层的类。当只将值放入结构中时,请使用超级通配符。
注意:通配符?表示零或一次,表示未知类型。通配符可以用作参数的类型,而不能用作泛型方法调用或泛型类实例创建的类型参数。(即,当使用通配符时,引用不在程序的其他地方使用,如我们使用T)
import java.util.ArrayList;
import java.util.List;
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 {
public static void main(String[] args) {
//? extends Shape i.e. can use any sub type of Shape, here Shape is Upper Bound in inheritance hierarchy
List<? extends Shape> intList5 = new ArrayList<Shape>();
List<? extends Shape> intList6 = new ArrayList<Cricle>();
List<? extends Shape> intList7 = new ArrayList<Rectangle>();
List<? extends Shape> intList9 = new ArrayList<Object>();//ERROR.
//? super Shape i.e. can use any super type of Shape, here Shape is Lower Bound in inheritance hierarchy
List<? super Shape> inList5 = new ArrayList<Shape>();
List<? super Shape> inList6 = new ArrayList<Object>();
List<? super Shape> inList7 = new ArrayList<Circle>(); //ERROR.
//-----------------------------------------------------------
Circle circle = new Circle();
Shape shape = circle; // OK. Circle IS-A Shape
List<Circle> circles = new ArrayList<>();
List<Shape> shapes = circles; // ERROR. List<Circle> is not subtype of List<Shape> even when Circle IS-A Shape
List<? extends Circle> circles2 = new ArrayList<>();
List<? extends Shape> shapes2 = circles2; // OK. List<? extends Circle> is subtype of List<? extends Shape>
//-----------------------------------------------------------
Shape shape2 = new Shape();
Circle circle2= (Circle) shape2; // OK. with type casting
List<Shape> shapes3 = new ArrayList<>();
List<Circle> circles3 = shapes3; //ERROR. List<Circle> is not subtype of List<Shape> even Circle is subetype of Shape
List<? super Shape> shapes4 = new ArrayList<>();
List<? super Circle> circles4 = shapes4; //OK.
}
/*
* Example for an upper bound wildcard (Get values i.e Producer `extends`)
*
* */
public void testCoVariance(List<? extends Shape> list) {
list.add(new Object());//ERROR
list.add(new Shape()); //ERROR
list.add(new Circle()); // ERROR
list.add(new Square()); // ERROR
list.add(new Rectangle()); // ERROR
Shape shape= list.get(0);//OK 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 Object());//ERROR
list.add(new Shape());//OK
list.add(new Circle());//OK
list.add(new Square());//OK
list.add(new Rectangle());//OK
Shape shape= list.get(0); // ERROR. Type mismatch, so list acts only as consumer
Object object= list.get(0); //OK 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.
*/
}
}
泛型和示例
协方差和反方差根据类型确定兼容性。在任何一种情况下,方差都是有向关系。协方差可以翻译为“同一方向不同”或不同,而反方差意味着“相反方向不同”,或相反。协变型和逆变型并不相同,但它们之间存在相关性。这些名称暗示了相关性的方向。
https://stackoverflow.com/a/54576828/1697099https://stackoverflow.com/a/64888058/1697099
协方差:接受子类型(只读,即Producer)相反:接受超类型(仅写,即Consumer)