协变、逆变和不变
arrays in Java are covariant, generics are not, they are invariant
TL;DR:
- 符号:<后 extend 前>、<前 super 后> (前 代表父类,时间上为前;后 代表子类,时间上为后)
- java中对于泛型型变只是一种约束;
- 泛型 = 类型安全的万能匹配;// 类似”模板代码“的技术;
Java泛型使用类型擦除( Type Erasure )实现, 所有的工作都是编译器做的, jvm并不知道泛型的存在
( Object 替代了 T )。
通过查看Mybatis的分页的源码PageVO<T extends BaseVO>
,发现自己不太明白这个extends的含义,经过学习就有了本文的探讨;
一. 定义1
设Orange类, 为Fruit类的子类,以泛型集合类List<T>为例:
型变: 分为逆变和协变, 与不变对应, 用来描述类型转换后的继承关系;
- 协变(covariant):也就是说,Crate<Orange> 是 Crate<? extends Fruit> 的子类型。上界 Fruit;
- 逆变(contravariant):也就是说,Crate<Fruit> 是 Crate<? super Orange> 的子类型。下界 Orange;
- 不变(invariant):也就是说,Crate<Orange> 和 Crate<Fruit> 之间没有关系。是不同对象;
把对象想象成一颗从 Object 往下的树,能更好理解上下界。
返回类型只能用协变(covariance),返回类型是接口或委托定义返回类型的子类,
参数类型只能用逆变(contravariance),参数类型是接口或委托定义的参数类型的父类。
- 生产者(Producer): 只能读取的对象; (没有消费者方法的对象)
- 生成者方法: 返回类型为T的方法 ( T getObject( ) )
- 消费者(Consumer): 只能写入的对象; (没有生产者方法的对象)
- 消费者方法: 参数带T的方法 ( void setObject( T t ) )
- 子类型(subtype) 不等于 子类(subclass)
- 子类一定是子类型,子类型不一定是子类
Crate<Orange>
是Crate<? extends Fruit>
的子类型,Crate<Orange>
不是Crate<? extends Fruit>
子类
二. 目的
都是为了增加灵活性, 而做的约束;
对于协变 extends, 只 get;保证泛型类的类型安全;
对于逆变 super, 只 set;"get()"本身是类型安全的, 只是因为不确定返回的类型, 所以需要加以限制;
通过编译器, 限制泛型类上某些方法 ( producer method/consumer method ) 的调用;
三. PECS (全称 Producer Extends Consumer Super)
-
重要
使用 extends 确定上界的只能是生产者,只能往外生产东西,取出的就是上界类型。不能往里塞东西。
使用 super 确定下界的只能做消费者,只能往里塞东西。取出的因为无法确定类型只能转成 Object 类型 -
[extends get] 协变指定了上界(限定父类,允许往下的子类), 限制调用 set 方法, 允许调用get来保证类型的安全
List<Orange> oranges= new ArrayList<Orange>();
oranges.add(new Orange());
List<? extends Fruit> fruits = orange;
Fruit f = fruits.get(0);
- [super set] 逆变限定了下界(限定子类,允许往上的父类), 允许调用 set 方法, 并可以将objects赋值给它
List<Fruit> fruits= new ArrayList<Fruit>();
fruits.add(new Fruit());
List<? super Orange> objs = fruits;
objs.add(new Orange());
objs.add(new Banana());
objs.add(new Fruit());
//无法安全地读取,canContainFruits完全可能包含Fruit基类的对象,比如这里的Object
//Fruit f = objs.get(0); // wrong
Object o = objs.get(1); // right
- 协变和逆变互为反方向,在协变中我们可以安全地从泛型类中读取(从一个方法中返回),而在逆变中我们可以安全地向泛型类中写入(传递给一个方法)
ps:
如果一个范型类ClassCase<T>
使用 extends, 就可以使用T get(Object o)
方法, 不能使用Object set(T t)
方法
如果一个范型类ClassCase<T>
使用 super, 就可以使用Object set(T t)
方法, 不能使用T get(Object o)
方法
ObjectService os = new ObjectService();
Function<? extends ObjectService, ?> funce;
Function<? super ObjectService, ?> funcs;
Function<ObjectService, ?> function = objectService -> null;
// 都能赋值成功
funce = function;
funcs = function;
// extends 无法接收入参 无解
funce.apply(os); // error
funcs.apply(os);
Function<?, ? extends ObjectService> funcie;
Function<?, ? super ObjectService> funcis;
Function<?, ObjectService> function1 = s -> os;
// 都能赋值成功
funcie = function1;
funcis = function1;
// super 无法返回对象 但可以变通: Object apply = funcis.apply(null);
ObjectService apply = funcis.apply(null); // error
ObjectService apply1 = funcie.apply(null);
上面的例子可以看出, super 是一个功能比 extends 差的方法, 实际上 super 就是 List<Object>
, 功能只是为了限制入参, 也只能作用于入参
四. 总结:
- 协变extends限定了通配符类型的上界, 可以称为上界通配符(Upper Bounds Wildcards),所以我们可以安全地从其中读取;(方便理解: 通过父类引用获取子类值)
- 比如我们在其他类使用时, 方法体里定义参数:
void get(Pair<? extends Number> p){ // 可以传入Pair<Integer>
方法体里面, p为Number类型引用, 实际类型以传入的为准(多态)
}
而如果是:
void get(Pair<Number> p){} // 不能传入Pair<Integer>
- java里, Pair<Integer>和Pair<Number>, 互相没有关系;
- 所以协变就是把泛型的T上限, 限定在了Number上; 使创造出来的Pair<? extends Number>和Pair<Integer>有继承关系, 前面一个是父类型; 协变不能set;
- 而super限定了通配符类型的下界,所以我们可以安全地向其中写入; 逆变不能get;
(方便理解: 子类属性比父类多, 父类属性更通用, 名义上子类通过父类属性赋值, 即通用属性赋值) - 生产与消费可以这样对应: Producer-Extends(协变get), Consumer-Super(逆变set)。
- btw, 都只是为了能传值取值; 协变规定了从父类型获取, 逆变规定了从子类型赋值;
五. 限定类型
之前的部分, 大多是定义: class XX<T>{} 使用在方法上: add(XX<? extends Number> p){}
现在介绍用在类上的限定类型:
public class Pair<T extends Number> {}
- 一句话解释, 该泛型定义在类级别, 在使用时, 只能新建为Number或者Number的子类, 是一种约束;
Pair<Number> p1 = null;
Pair<Integer> p2 = null;
Pair<Double> p3 = null;
....所有的Number子类
- super不能用在class定义处,只能用在方法参数;
六. 无限定通配符
- <?>通配符:Pair<?>是所有Pair的超类
1. <?> 和 <? extends Object>的异同
- 首先, 使用到了 extends 所以只能 producer, 所以一般使用在方法入参上
public static void printListWildCard(List<? extends Object> list)
public static void printListWildCard(List<?> list)
List<Integer> li = Arrays.asList(1, 2, 3);
printListWildCard(li);
- 工作方式相同. 因为 Object 是 Java 所有对象的超类, 基本上所有的东西都扩展了 Object. 因此, 这个方法也会处理一个 Integer 类型的 List
也就是说, 大多数情况下 <?> 和 <? extends Object> 是同一个意思
<T extends Object & Foo>
will cause T to become Object
under erasure, whereas with <T extends Foo>
it will become Foo
under erasure.
- 区别
首先加个概念, 可重构类型
(reifiable 可具体化的, 可实现的) 是指那些在编译时 未被擦除
的类型, 不可重构类型
是会被擦除的类型
被擦除的类型, 在 运行时 表达的信息更少 (相比编译时)
比如 List<String>
和 Map<Integer,String>
就 不可重构
, 编译器会擦除它们的类型, 并将它们分别视为列表 List 和映射 Map, 所以会更严格
现在回到上面的例子: 无界通配符类型, 也就是 List<?>
以及 Map<?, ?>
是可重构的 (可以理解为 不严格的
)
List<? extends Object>
是不可重构. 虽然微妙, 但这是一个显著的区别.
<?> is reifiable while <? extend object> is not. The reason they did this is to make it easier to distinguish reifiable type. Anything that looks like <? extends something>,<T>,<Integer> are nonreifiable.
List someList = new ArrayList<>();
boolean instanceTest = someList instanceof List<?>; // true List<?> = List、List<Object>
boolean instanceTest = someList instanceof List<? extends Object>; // false ~ == List<Object>
七. 自限定类型(Self-Bounds Types)
Java中常用的自限定写法:
class Pair<T extends Base<T>> {
T element;
T get() {
return element;
}
void set(T t) {
element = t;
}
}
- class Subtype extends BasicHolder<Subtype> {}这样用,就构成自限定了。从定义上来说,它继承的父类的类型参数是它自己。
- 自限定的用法:父类作为一个泛型类或泛型接口,用子类的名字作为其类型参数。
- 这里再把Subtype抽象成类型参数T,刚好变成了T extends SelfBounded<T>这样的写法。
- 暂时埋个坑, 以后补
这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域。
E: Element
K: Key
V: Value
N: Number
T: Type
八、关于 List
、List<?>
、List<Object>
的区别
容器类使用泛型的好处:
- 安全性:在对参数化类型的容器中放入了错误即不匹配的类型的时候,编译器将会强制性进行错误提示
- 便利性:当从容器中取出元素的时候不用自己手动将
Object
转换为元素的实际类型了,编译器将隐式地进行自动转换 - 表述性:带有类型实参的泛型即参数化类型,可以让人看到实参就知道里面的元素
E
都是什么类型
引用变量的类型 | 名称 | 可以接受的类型 | 能否添加元素 | 安全性 | 便利性 | 表述性 |
---|---|---|---|---|---|---|
List | 原始类型 | 任何对应 List<E> 的参数化类型 (包括 List<?> ) | 可添加任意类型的元素 | 无 | 无 | 无 |
List<?> | 通配符类型 | 以接受任何对应 List<E> 的参数化类型 (包括 List ) | 不能添加任何元素 | 有 | 无 | 有 |
List<Object> | 实际类型参数为 Object 的参数化类型 | 仅接受 List 和其本身类型 | 可添加任意类型元素 | 有 | 有 | 有 |
-
List<?> 缺点在于不能添加任何元素, 只能使用
-
泛型的子类型化的原则:
List<String>
类型是原生类型List
的一个子类型, 而不是参数化类型List<Object>
的子类型
“?” is inclusive of “Object” in the class hierarchy. You could say that String is a type of Object and Object is a type of ?. Not everything matches Object, but everything matches ?.
List<Object> object = Lists.newArrayList();
void testQuery(List<?> l) {}
testQuery(object); // compiles because any list will work
List<?> query = Lists.newArrayList();
void testObject(List<Object> l) {}
testObject(query); // fails because a ? might not be an Object
but,
List<?> query = Lists.newArrayList();
List<Object> hack = List<Object> query;
testObject(hack); // should work~
// void testObject(List<Object> l) {}
so,
// 编译时
List<String> -> List
List<Object> -> List
// 编码时
List<Any> 赋值 -> List<?> (代价是只能get了)
// 运行时
List<?> ≈ List / List<Object>
说到底, 只是一个编译时的规定
编译时和运行时
Java 有 编译期、运行期 两个阶段,
- 编译期: 源代码 在 编译器
- 运行期: 字节码 在 虚拟机
因为存在“编译后泛型擦除”, Java的泛型也被称为“假泛型”
-
泛型是JDK专门为编译器创造的语法糖,只在编译期,由编译器负责解析,虚拟机不知情
- 存入:普通类继承泛型类并给变量类型T赋值后,就能强制让编译器帮忙进行类型校验
- 取出:代码编译时,编译器底层会根据实际类型参数自动进行类型转换,无需程序员在外部手动强转
-
实际上,编译后的Class文件还是JDK1.5以前的样子,虚拟机看到的仍然是 Object
-
泛型有以下作用:
- 抽取代码模板:代码复用并且可以通过指定类型参数与编译器达成约定
- 类型校验:编译时阻止不匹配元素进入
Object[]
- 类型强转:根据泛型自动强转(多态,向下转型)
在大家开始慢慢觉得泛型只和编译器有关,但实际上泛型的成功离不开多态
代码模板的本质就是:用Object接收一切对象,用泛型+编译器限定特定对象,用多态支持类型强转。
泛型只是程序员和编译器的约定,程序员告诉编译器,我假定这个List只能存String,你帮我盯着点。对于存入的方法,如果不小心放错类型,就编译报错提醒我。对于取出的方法,编译时你根据我给的实际类型参数自动帮我类型转换吧
- 对象类型弄成了变量吗?
通过反编译大家也看到了,其实根本没有所谓的泛型类型T,底层还是Object
,所以当我们new一个ArrayList
时,JVM根本不会傻傻等着T被确定。T作为参数类型,只作用于编译阶段,用来限制存入和强转取出,JVM是感知不到的,它不关心这个对象将来是 ArrayList<Integer>
还是ArrayList<String>
,仍然还是按JDK1.4以前的做法,底层准备Object[]
,以便利用多态特性接收任意类型的对象。
更何况JVM实际new对象是在运行期,编译期的小把戏和它有什么关系?
所以对于泛型类本身来说,它的类型是确定的,就是Object
或Object[]
数组
- 为什么泛型不支持基本类型
JDK1.5 不仅引入泛型,还同时发布自动拆装箱特性, 在这之前 Object obj = 666;
是不能赋值的
但自动拆装箱特性, 也只是一个语法糖级别的特性, 也就是功能上支持了, 但语法层面还是不支持的; 碰上数组之类的, 赋值必然需要诶个拆箱, 带来性能损耗, 属于是既不允许也没必要
JAVA动态绑定机制
编译类型 or 运行类型
- 编译时类型由声明该变量时使用的类型决定, 编译时引用变量只能调用其编译类型所具有的方法
- 运行时类型由实际赋给该变量的对象决定
动态绑定机制是绑定运行类型
- 调用对象方法的时, 方法会和该对象的 内存地址/运行类型 绑定
- 调用对象属性时,没有动态绑定机制,哪里声明哪里使用
https://www.jianshu.com/p/0c2948f7e656 ↩︎