标题其实是回环:Generics(泛型)的参与、不变与Collection(集合)的参数、不变。?
Class(类)的继承关系想必大家都很熟悉,Java从5开始有了Genertics,即允许对一类相同类型的Object进行操作,增强了编程的宏观性和代码可维护性,避免过度陷入具体数据类型的细节中。用<E>代表一类都属于(Class)E的Object,声明创建针对<E>的class或method不需要过度考虑E的细节,比如进行Collection的添加、输出操作涉及Integer和Double两个类型数据,把E改成它们父类的Number即可。
关于covariance(协变)和invariance(不变): subclass产生的object的集合(如array)自动是superclass产生object的集合的子集,而sub-parameter产生的collection(如Collection<Integer> integers)则不是super-parameter(e.g. Collection<Number> numbers)产生的collection的子集。这对方法产生了明显影响,比如把全是Integer的array加入到一个全是Number(其中可以是Interger, Double, Float..whatever)的array里没有问题,能对后一个array进行的操作也一定能对前一个进行。但能对<Number>的collection进行的操作则不能进行到<Integer>的collection里,具体错误显示为编译错误的不兼容类型。
这部分背景知识参见:
[1]https://stackoverflow.com/questions/2991821/java-inheritance-doubt-in-parameterised-collection
[2]https://stackoverflow.com/questions/18666710/why-are-arrays-covariant-but-generics-are-invariant
[3]https://www.jianshu.com/p/90948ff4a940
[4]https://www.cl.cam.ac.uk/teaching/1516/ConceptsPL/variancetypo.pdf
其中[2]的第一个回答用猫狗、动物的例子做了非常直观的回答,比如一个<Animal>的collection "animals",cat1, dog1, cat2都在里面,而<Cat>的collection "cats"则只包含cat3, cat4。这时候定义一个方法,给animals加入dog2,没有问题,但这个操作显然不能给cats,因为dog和cat是并列也是矛盾的两个class。
这种情况在array里允许,根据那条回答的评论以及其他文章,一个重要原因是安全性,要避免generics过度自由使用情况下造成的运行错误,而array这类“更具体”的数据类型产生问题易排查易修复,造成的问题规模比generics小。
依据教材做练习时候遇到了一些有趣的情况,把它们发出来作为parameter和invariance的实例分析编译器的行为
1.正确编译部分
作为静态语言,正确编译是能够运行的前提。
1.1 源码
以之前定义的Shape作为类型参数
static void printShapeCollection(Collection<Shape> collection){
for (Shape shape:collection)
System.out.println(shape);
}
1.2
static void printShapeCollection(Collection<? extends Shape> collection){
for (Shape shape:collection)
System.out.println(shape);
}
参数框里的问号叫wildcard(通配符),其语义试具体语境而定,这里“ ? extends Shape” 知道Shape是class,问好能代表的也一定是class。这里的类型参数扩大了参数范围,因为如之前所言collection的不同参数即使有继承关系也互相独立,子参数(类)产生的collection不是父参数(类)产生的collection的子集,而通配符的加入让方法里可以接受的实例组成的collection由单纯必须以collection声明增加了Circle, Triangle, Cone, Rectangle等;Collection <Shape>本身也可以,这是有趣的一点,一个类自己自动被视为继承自己的。
1.3 (以下省略重复部分)
printShapeCollection(Collection<? extends Shape> collection){
for (Object shape:collection)
这里方法内参数由具体的类变成了它们的最高级Object,任何class都extends Object,Object supers任何其他class。这里编译器能通过我猜是因为Shape和其subclass(es)与Object可以互相转化;把Object改成Circle编译报错(见2.2)是因为 “? extends Shape” 和Circle不等同的【至少包括Shape本身】肯定存在可以在Shape实例的collection层面进行但不能在Circle实例的collection层面进行的操作(比如加入实例cone,跟动物里加狗但猫里不能加一样),但这里Object扩大了实例的范围,万一collection里的object(s)正好都能满足shapes呢,可见编译器还是比较宽容的。
1.4
printShapeCollection(Collection<? super Circle> collection){
for (Object shape:collection)
方法内参数扩大为Object与“? super Circle ”搭配,注意super是动词复数。
2 .编译错误例子
2.1
printShapeCollection(Collection<? super Circle> collection){
for (Shape shape:collection)
这个显示不兼容的类型,(方法内)要求Circle而找到的是Shape,其实Shape是supers Circle的,我的解释是:编译器没法在编译中逆向判断继承关系。
注意:方法内参数改成Object则编译通过且正确运行【加入为1.5】,因为Object是一定supers Circle的,这可能是一个根据类的层级关系自动判断非没有进行逆向搜索+判断的过程。
2.2
printShapeCollection(Collection<? extends Shape> collection){
for (Circle shape:collection)
编译器报错方式与2.1正好调过来,(方法内)要求Shape而找到Circle,因为之前定义的shapes是以<Shape>为参数的,不是<Circle>
Collection<Shape> shapes = new BasicCollection<Shape>();
【之前的错误推测:“这里可以推测编译器对于collection的类型参数不但不能逆向判断继承关系,连正向的判断也不进行”】。
2.3
由2.2自动能推出2.3。设2.2和2.3的情况分别为B、C,编译不通过的集合为X,已知B是X的子集,而C是B的子集,所以C也是X的子集【集合的包含关系有传递性?】
printShapeCollection(Collection<Shape> collection){
for (Circle shape:collection)
2.4
printShapeCollection(Collection<? extends Circle> collection){
for (Shape shape:collection)
这个显示不兼容的类型,Shape无法转换成Circle,编译报错的位置不是这个方法,而是这个方法的应用
printShapeCollection(shapes);
方法创建部分编译通过的原因与1.3相同,即Shape可以兼容Circle,然而shapes不是以<Circle>而是以<Shape>创建的(2.2),用参数类型为<Circle的子类>【包含Circle本身,参考集合论的“子集”?】给一个和它不同的collection下命令正好撞在generics collection的invariance枪口上。
总结:
1 根据原例子进行的实验中,编译成功的全部正确运行,没有出现运行错误,显示了泛型Collection基于类型参数进行的约束限制保证了安全性、严整性。
2 编译器判断原则是,方法里的实例类型(产生实例的类)是不是方法签名中参数的子类/与之兼容,只要方法里的实例类型跟参数里的对得上就编译通过.
3.泛型和collection明显建立在集合论的基础上,种种限制、规定和允许的情况延续了一般数理逻辑的处理方式,未必是最好但这是大家通行的,进一步展示了计算机/计算理论中数学的底层基础地位。
展望:
Javadoc官方文档非常有用,但对于初学的我来说读起来很累,因为它是自成一体的(self-contained),设计算法等普遍计算理论的时候容易建立接口,但设计Java语法本身时候只能以Java理解Java。Java语法是表层,编译原理甚至Java虚拟机是底层,目前有幸对底层有了非常肤浅的接触,进一步更有效的学习如何进行还有待探索+欢迎大家建议!