【Week1】Java 与 Kotlin 泛型和通配符深入解析

问题

今天小陈提给我一个问题:

	Class B extends A {}
	List<A> listA;
	List<B> listB;
	listA = listB; //语句1
	listB = listA; //语句2

语句1 和 语句2 是否正确,是否可以编译通过?

初步分析

按照 Java 的多态特性,类 B 是类 A 的子类,那么我们自然会想到,如果是以下的使用,是可以编译通过并且正常使用的:

	A a;
	B b;
	a = b;

然而这里我们并不能简单的类比,认为 语句1 listA = listB 是可以成立的,具体的原因在后面会分析得到。而 语句2 ,哪怕是从多态的角度看,也是不成立的。

泛型

我们知道,List 是 Collection 的子类,是 Java 中的容器接口类,而之所以要设置容器类,就是想用同一个容器类来装不同的具体的类的对象,而不是定义 IntegerList、StringList 等多个类,但实际上他们都是 List 类,为了实现这个容器的效果,Java 引入了【泛型】的概念。

泛型的概念听起来比较难懂,其实可以理解为,把具体的数据类型,作为参数传入泛型中,这种参数可传入类、接口和方法,分别被称之为泛型类、泛型接口、泛型方法。

有了泛型的概念,就可以理解 List 这样的容器,实际是通过传入 < T > 的泛型参数,来用同一个容器类,承载不同的实例对象。当直接使用 List ,当不传入具体类型参数 < T > 时,实际是等同于 List < Object >。

然而泛型,其实是 Java 引入的一个语法概念,只在编译阶段有效,在经过了编译后,泛型其实会被擦除类型,关于这一点,可以打印 List< A >和 List< B > 的 getClass() 方法结果来进行验证。
在这里插入图片描述
这就解释了,为什么前面提到的 listA = listB 语句是不被允许的,因为在类型擦除后,编译器会将他们都变成 List ,如果允许这样的操作,在取出元素时,就会造成类型不匹配的异常。因此 Java 用语法的强制方式,直接禁止了这个操作。

通配符

然而我们在实际的使用中,的确会有这样的使用场景,Java 提供了【通配符】来实现这样的场景。

上界通配符

还是延续之前的 B extends A 的设定,把之前的 listA = listB,改成以下的写法:

	List<B> listB = new ArrayList<>();
    List<? extends A> listA = listB; //语句3

这里的 ? extends A 就是上界通配符,B 满足 ? extends A 的限定条件,所以这个操作是通过的。同样的,List <?> 等同于 List <? extends Object> ,也就是任意一个类都满足这个条件。总结来说,就是 添加进 listA 中的元素的类型需要满足 extends A ,也就是 A 的子类的条件。(需要注明的是:这里的 extends 实际包含了 Java 语法中的 extends 和 implement 两种情景,也就是当 A 是接口类时,实现了这个接口类的所有类都满足通配符条件)

以下操作都是可以通过的:

	Class C extends B {}
	List<? extends A> listA = new ArrayList<A>(); //语句4
	List<? extends A> listA = new ArrayList<C>(); //语句5

get 和 add ,是 List 的两个常规操作,但是加上通配符后,这两个操作有什么不同吗?

首先是 get 方法:

    List<B> listB = new ArrayList<>();
    List<? extends A> listA = listB;
	A a = list.get(0); //语句6
	B b = list.get(0); //语句7

先看 语句6 ,根据通配符的规则设定, 添加进 listA 中的元素的类型需要满足 extends A ,也就是 A 的子类的条件,根据多态的特性,A 的子类对象是可以赋值给 A 类型的对象的,所以 语句6 可以通过。

但对于 语句7 ,由于 A 的其他子类都可以被添加进 listA 中,那么并不能确定取出来的对象是 B 类型,自然也是不能被赋值给对象b 的,语句7 不通过。

接下来看 add 方法:

	A a;
	B b;
	List<? extends A> listA;
	listA.add(a); //语句8
	listA.add(b); //语句9

根据前面的结论,语句4 和 语句5 都可以通过,那上面的 语句8 和 语句9 两个 add 语句,是否也能通过呢?答案是否定的。

因为对于 listA 而言,列表里的元素只需要满足通配符的范围条件。但是对编译器并没办法确定 listA 是 List< A > ,还是 List < B >。那么当通配符 ? 代表 B 类型,也就是 listA 是 List < B > 时,如果向里面 add 对象 a ,显然是不能通过的。 由于无法确定这一点,Java 就同样用语法的方式,禁止对通配符限定的 List 做 add 操作,在这样操作时,会编译不通过并且出现如下图的提示。
在这里插入图片描述

下界通配符

既然有上界通配符 ? extends A ,当然也有下界通配符 ? super B,类比的来理解,这里的 ? 就代表了,满足是 B 的父类条件的任意类。如 B 直接 extends 的 A ,间接继承的 Object 类,或者是 B implement 实现的接口 D,都满足这个条件。

	A a;
	B b;
	List<? super B> listB;
	listB = new ArrayList<A>(); //语句10
	listB = new ArrayList<Object>(); //语句11
	
	Object element = listB.get(0); //语句12
	listB.add(b); //语句13
	listB.add(element); //语句14
	listB.add(a); //语句15

如上,对于设置了下界通配符范围的 listB ,语句10,语句11 的赋值语句都是满足条件的,自然可以通过。

语句12 的 get 语句也是可以通过的,但需要注意的是,下界通配符修饰的 List 类 get 的元素,只能为 Object 类型。按照之前同样的理解方式,编译器无法确定满足 ? super B 条件的 ? 代表的真正的类型,所以只能用所有类的公共父类 Object 类,如果需要转成具体的类型,需要再多进行一步强转操作。

再看 语句13 ,由于通配符限制的 ? 类一定是 B 的父类,那么根据多态的特性,无论 ?具体代表的类是什么,B 的对象都可以转成父类 ? 的对象,所以 语句13 是不会引起异常或争议,可以正常编译通过的。

那么对于 语句15 和 语句16 ,就是不能通过的了。因为比如 ? 代表 B 的直接父类 A ,那么 Object 类型的 elment 并不能转化成 A 类型,也就无法 add。同样的如果 ? 代表的是 B implement 的接口类 D,那么 A 同样也无法成功转化,当然也就无法被 add 进去。为了避免异常情况的发生,只能通过语法的方式,禁止这样的添加。

经过上面的分析,Java 的泛型和通配符就总结的差不多了。在实际的使用场景中,上界通配符 ? extends A 往往用于有 get 需求的 List 场景;而下界修饰符由于 get 到的元素只能是 Object 类型,还需要进行强转,所以用于 add 需求的情况比较多。

Kotlin 的 in 和 out 关键字

分析完 Java 的泛型和通配符,也需要知道 Kotlin 对于这样的场景,是如何处理的。

所以 Kotlin 用了关键词:out 和 in ,分别对应了 Java 的 上界修饰符 ? extends A 和下界修饰符 ? super B ,而 Java 中的 List < ? >,对应到 Kotlin 就是用 List < * >。

var listA: List<out A>
var listB: List<in B>
var list: List<*>

简单从字面意义理解,out 指的是用于从容器中取出元素;in 指的是用于向容器中添加元素,这就对应了我们前面 Java 通配符总结的最后一段。

如果不了解 Java 的通配符,单看这两个关键词,其实会容易产生迷惑和混淆的感觉。但根据我们前面对 Java 通配符的分析,再理解这两个关键词,就会了解为什么是 out 和 in 了,out 和 in 其实也可以分别理解成我们经常说的【消费者】:只能读而不能写,和【生产者】:只能写而不能读。

Java 和 Kotlin 对泛型的处理其实没什么太大差异,最大的差异就在于命名,用法以及体现出的两个语言不同的设计思想。

Java 作为一个底层语言,对于泛型和通配符的命名和设计,其实是自下向上进行设计,泛型的设计思路与 Java 底层的多态,继承等基础特性息息相关。

而 Kotlin 作为一个上层语言,面向的不只是 Java ,还会支持 JS 等其他语言,所以 Kotlin 在设计时,更多的是站在一个从上到下的角度,直观的体现使用场景,这样不论底层语言的设计有什么差别,都能够从字面意义上明白,需要读的时候就用 out ,需要写时就用 in。

其他的用法上的区别就不再赘述,总而言之,本文重点介绍了 Java 泛型和通配符的设计思想和实际用法限制,同时对比了 Kotlin 和 Java 处理上的差异,以及其中体现的语言思想的差异。

在研究泛型的过程中,其实可以发现有许多我们平时用到的 Java 语法,其实是因为编译器本身无法处理或者语言为了避免异常而设计的。关于这个有趣的场景,后面我也想单独写一篇文章,总结下其他类似的场景。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值