@
GuuJiang 幹,想换行时老是习惯性按 cmd+enter ,又发出来了,书接上文。。。
把这三个类型写成下面的形式可能会更容易理解
List<T>
@
CovariantList<T>
@
ContravariantList<T>
也就是说给 List<T>这个类型额外指定一个 variance 属性,variance 可以有三种取值,分别是 Invariant(默认)、Covariant 和 Contravariant ,下面给出这三种关系的正式定义
如果 B 是 A 的子类,则 List<B>是 List<A>的子类,那么这样的类型系统称为协变(covariant)
如果 B 是 A 的子类,则 List<A>是 List<B>的子类,那么这样的类型系统称为逆变(contravariant)
如果不管 A 和 B 之间是否有子类关系,List<A>和 List<B>之间都没有子类关系,那么这样的类型系统称为不变(invariant)
所以? extends 和? super 语法的本质是,在 Java 的类型系统默认为 invariant 的前提下,人为指定某个类型的 variance 属性,使其变成 covariant 的或者 contravariant 的
到此为止,我们搞清了第一个问题,即为什么要有协变和逆变,在默认情况下,如果有一个方法的一个参数类型为 List<Number>,那么在使用这个方法时它只接受 List<Number>,而如果将其指定为协变的,它就可以接受 List<Integer>,如果将其指定为逆变的,它就可以接受 List<Object>,所以就好像范型的出现扩展了方法的适用范围一样,协变和逆变的出现进一步扩展了方法的适用范围
下面就到了第二个问题,这个方法的参数可以接收的范围广了以后,对这个参数的值的使用上就和原来有区别了,需要受到一些限制,而这个限制就是被无数人提起过的 PECS 原则,话说 PECS 是第二个我个人不太喜欢的概念,确实,它是一个非常精妙的总结,使得看过它的人能够很容易地记忆对于协变或者逆变后的类型在使用上的限制,但是它同时也带来了另一个误解,PECS 里的“生产”、“消费”等概念假定了范型类型一定是某种“容器”,诚然,用到范型最多的场景确实就是容器,就好像我写的这段话一样,提到范型的时候第一反应也是拿 List 来举例,但是事实上,范型和容器之间没有任何必然联系,对于编译器来说也不存在“生产”、“消费”等业务层面的概念,那么“生产”和“消费”的本质到底是什么呢?
写到这里回头补充下前面的一段,先从简单类型入手了解下类型约束的本质是什么,前面提到的著名误解是误认为范型约束约束的是容器类型和容器内对象的类型,那实际上后者到底是由谁来约束的呢,事实上,如果有一个 List<T>,那这个 List 里能放什么类型的对象呢,答案是 T 及 T 的子类,这个相信每个人都知道,但是深入想想,类型系统是编译器关心的事情,而“一个类型为 List<T>的 list 里面能放什么类型的对象”这个问题明显是个业务层面的问题,对于编译器来说,“容器”、“容器里元素的类型”等这些概念都是不存在的,那我们天天挂在嘴上的“List<T>里能够存放 T 及其子类”这个结论到底是由谁来保证的呢,真正的答案是,List<T>里方法 add 的签名为 add(T),那根据里氏代换原则,这个参数自然接受 T 及其子类都是合法的,这才是这个约束的本质,编译器只负责校验方法的签名,保证了 add 方法只能接受 T 及其子类的参数,最终产生的效果才是我们说的“List<T>里能够存放什么”这个问题
回到 PECS 这边来,所以说,对于编译器而言,“生产”的本质是“调用返回值为 T 的方法”,“消费”的本质是“调用参数为 T 的方法”,理解了这一点,PECS 自然就是顺理成章的了,而且根本不用去记,随便推导一下就能得出答案,下面试验一下按照上述的方法从头推导 PECS 原则,假如现在有一个引用 List<? extends Number> l ,那么下面这个语句是否是合法的
Number n = l.get(0)
答案是是,因为协变原则保证了 l 的实际类型可能为 List<Number>、List<Integer>、List<Float>等,无论是那种,其 get 方法的返回值肯定是 Number 或 Number 的子类,因此将这个返回值赋值给 Number 是完全没有问题的,反之,如果 l 的类型是 List<? super Number>,那么它的实际类型可能是 List<Number>、List<Object>等,因此是无法保证 add 的返回值一定能够赋值给 Number 的
反之,对于如下的语句
Number n = ...
l.add(n)
如果 l 是 List<? super Number>,那么不管它的实际类型是什么,其 add 方法一定能够接收 Number 类型的参数,而如果 l 是 List<? extends Number>,就无法保证其 add 方法一定能够接收 Number 类型的参数
以上才是 PECS 原则的本质
想到哪写到哪,一不小心写了这么多,可能有点啰嗦,其实相信对于很多人来说,看到讲误解的那一段应该就能自己想通后面的这些内容了,权当是自己的一份笔记吧,希望这篇回答可以就此终结所有关于范型约束的月经问题
最后再补充一点,凡是提到 Java 的范型相关的问题总有人要提到擦除法,实际上对于今天的这个问题来说,和擦除法没有任何的关系,因为这些都是类型系统相关的问题,是编译阶段处理的问题,而擦除法是运行阶段的问题,二者之间没有任何联系,不管 Java 采不采用擦除法,今天讨论的这个问题的结论都不会有任何变化