Java泛型的协变、逆变和不变

通过PageVO引出的泛型思考:

TL;DR: java中对于泛型型变只是一种约束; 和kotlin等其他语言并不等同.
泛型: 类型安全万能匹配, 一种类似”模板代码“的技术,
Java泛型使用擦拭法( Type Erasure )实现, 所有的工作都是编译器做的, jvm并不知道泛型的存在( Object替代T )。

一. 定义

设Orange类, 为Fruit类的子类,以泛型集合类List<T>为例:

型变: 分为逆变协变, 与不变对应, 用来描述类型转换后的继承关系;

  • 协变(covariant):也就是说,Crate<Orange> 是–> Crate<? extends Fruit> 的子类型。
  • 逆变(contravariant):也就是说,Crate<Fruit> 是–> Crate<? super Orange> 的子类型。
  • 不变(invariant):也就是说,Crate<Orange> 和 Crate<Fruit> 之间没有关系。

  • 生产者(Producer): 只能读取的对象; (没有消费者方法的对象)
    • 生成者方法: 返回类型为T的方法 ( T getXX( ) )
  • 消费者(Consumer): 只能写入的对象; (没有生产者方法的对象)
    • 消费者方法: 参数带T的方法 ( void setXX( T t ) )

  • 子类型(subtype)不等于子类(subclass)
    • 子类一定是子类型,如 Crate<Orange> 是 Crate<? extends Fruit> 的子类型
    • 子类型不一定是子类,如 Crate<Orange> 并不是 Crate<? extends Fruit> 的子类

二. 目的

都是为了增加API灵活性, 而做的约束;
对于协变-extends, 只get; 保证泛型类的类型安全;
对于逆变-super, 只set; "get()"本身是类型安全的, 只是因为不确定返回的类型, 所以需要加以限制;
通过编译器, 限制泛型类上某些方法 ( producer method/consumer method ) 的调用;

三. 例子

协变指定了上界, 限制了set, 允许调用get来保证类型的安全; (extends get)

List<Orange> oranges= new ArrayList<Orange>();
oranges.add(new Orange());
List<? extends Fruit> fruits = orange;
Fruit f = fruits.get(0);

逆变限定了下界为Fruit的List, 允许调用set, 并可以将objects赋值给它。(super set);

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 = canContainFruits.get(0);
Fruit o = objs.get(1);

协变和逆变互为反方向,在协变中我们可以安全地从泛型类中读取(从一个方法中返回),而在逆变中我们可以安全地向泛型类中写入(传递给一个方法)。

四. 总结:

  • 协变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的超类; ( 使用方法暂无了解 )

七. 自限定类型(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>这样的写法。
  • 暂时埋个坑, [以后补][https://blog.csdn.net/lengjiayi/article/details/85223878]
    这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域。

六. 类型参数的命名约定

类型参数一般使用一个大写的字母表示,经常使用的类型参数的名称有

E: Element(广泛的用于Java Collection中)
K: Key
N: Number
T: Type
V: Value
S,U,V: 第2, 3, 4个类型参数

猜你喜欢

转载自blog.csdn.net/howeres/article/details/108320729