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

协变、逆变和不变

arrays in Java are covariant, generics are not, they are invariant

TL;DR:

  1. 符号:<后 extend 前>、<前 super 后> (前 代表父类,时间上为前;后 代表子类,时间上为后)
  2. java中对于泛型型变只是一种约束;
  3. 泛型 = 类型安全万能匹配;// 类似”模板代码“的技术;

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);

在这里插入图片描述

  1. 工作方式相同. 因为 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.

  1. 区别

首先加个概念, 可重构类型 (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

八、关于 ListList<?>List<Object> 的区别

容器类使用泛型的好处:

  1. 安全性:在对参数化类型的容器中放入了错误即不匹配的类型的时候,编译器将会强制性进行错误提示
  2. 便利性:当从容器中取出元素的时候不用自己手动将 Object 转换为元素的实际类型了,编译器将隐式地进行自动转换
  3. 表述性:带有类型实参的泛型即参数化类型,可以让人看到实参就知道里面的元素 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对象是在运行期,编译期的小把戏和它有什么关系?

所以对于泛型类本身来说,它的类型是确定的,就是ObjectObject[]数组

  • 为什么泛型不支持基本类型

JDK1.5 不仅引入泛型,还同时发布自动拆装箱特性, 在这之前 Object obj = 666; 是不能赋值的

但自动拆装箱特性, 也只是一个语法糖级别的特性, 也就是功能上支持了, 但语法层面还是不支持的; 碰上数组之类的, 赋值必然需要诶个拆箱, 带来性能损耗, 属于是既不允许也没必要

JAVA动态绑定机制

编译类型 or 运行类型

  • 编译时类型由声明该变量时使用的类型决定, 编译时引用变量只能调用其编译类型所具有的方法
  • 运行时类型由实际赋给该变量的对象决定

动态绑定机制是绑定运行类型

  • 调用对象方法的时, 方法会和该对象的 内存地址/运行类型 绑定
  • 调用对象属性时,没有动态绑定机制,哪里声明哪里使用

  1. https://www.jianshu.com/p/0c2948f7e656 ↩︎

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java泛型中的协变逆变都是针对类转换的规定。 协变(covariant):指的是继承链中子类(派生类)类能够作为父类(基类)类的一种属性,也就是子类可以作为父类使用的能力。在泛型中,协变的概念可以用来表示如果类A是类B的一个子类,那么泛型类G<A>就可以视作泛型类G<B>的一个子类。 例子: ```java // Animal类 public class Animal {} // Dog类是Animal类的子类 public class Dog extends Animal {} // 泛型接口List public interface List<E> { void add(E e); E get(int index); } // 定义一个方法acceptList,其形参类为List<? extends Animal> public static void acceptList(List<? extends Animal> list) { for (Animal animal : list) { // ... } } // List类为List<Dog> List<Dog> list = new ArrayList<Dog>(); list.add(new Dog()); acceptList(list); // 在这里,我们可以传入一个List<Dog>参数,因为Dog类是Animal类的子类 ``` 逆变(contravariant):指的是继承链中父类(基类)类能够作为子类(派生类)类的一种属性,也就是父类可以作为子类使用的能力。在泛型中,逆变的概念可以用来表示如果类A是类B的一个超类,那么泛型类G<B>就可以视作泛型类G<A>的一个子类。 例子: ```java // Animal类 public class Animal {} // Dog类是Animal类的子类 public class Dog extends Animal {} // 泛型接口Comparator public interface Comparator<T> { int compare(T o1, T o2); } // 定义一个方法sortList,其形参类为List<? super Dog> public static void sortList(List<? super Dog> list) { // ... } // List类为List<Animal> List<Animal> list = new ArrayList<Animal>(); list.add(new Animal()); sortList(list); // 在这里,我们可以传入一个List<Animal>参数,因为Animal类是Dog类的超类 ``` extends和super关键字常常用于定义泛型参数的上边界(upper bound)和下边界(lower bound)。extends表示类参数的上限,超过这个范围就会导致编译错误;super表示类参数的下限,超过这个范围也会导致编译错误。 例子: ```java // 泛型类Pair,其类参数T有上限(用extends)为Comparable<? super T>,表示类T要么是Comparable<? super T>本身,要么是Comparable<? super T>的子类 public class Pair<T extends Comparable<? super T>> { private T first; private T second; public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public T max() { return first.compareTo(second) >= 0 ? first : second; } } // Pair类为Pair<String> Pair<String> pair = new Pair<String>("hello", "world"); String max = pair.max(); // 在这里,我们可以调用max方法,因为String类实现了Comparable<String>接口 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值