资深架构师和你谈谈 Kotlin 的泛型

if(“nanchen2251” instanceof T.class){

// 报错:Identifier expected Unexpected token

}

比如重载:

void func(T t){

// 报错:‘func(T)’ clashes with ‘func(E)’; both methods have same erasure

}

void func(E e){

}

同样,因为基本数据类型不属于 oop,所以也不能被擦除为 Object,所以 Java 的泛型也不能用于基本类型:

List list;

// 报错:Type argument cannot be of primitive type

oop:面向对象的程序设计(Object Oriented Programming)

到这里,是不是可以回答上面的第 3 个问题了:Java 的类型擦除到底是指什么?

首先你要明白一点,一个对象的类型永远不会被擦出的,比如你用一个 Object 去引用一个 Apple 对象,你还是可以获得到它的类型的。比如用 RTTI。

RTTI:运行时类型信息,运行时类型识别 (Run Time Type Identification)

Object object = new Apple();

System.out.println(object.getClass().getName());

// will print Apple

哪怕它是放到泛型里的。

class FruitShop{

private T t;

public void set(T t){

this.t = t;

}

public void showFruitName(){

System.out.println(t.getClass().getName());

}

}

FruitShop appleShop = new FruitShop();

appleShop.set(new Apple());

appleShop.showFruitName();

// will print Apple too

为啥?因为引用就是一个用来访问对象的标签而已,对象一直在堆上放着呢。

所以不要断章取义认为类型擦除就是把容器内对象的类型擦掉了,所谓的类型擦除,是指容器类FruitShop,对于 Apple 的类型声明在编译期的类型检查之后被擦掉,变为和 FruitShop 等同效果,也可以说是 FruitShop 和 FruitShop 被擦为和 FruitShop 等价,而不是指里面的对象本身的类型被擦掉!

那,Kotlin 中有类型擦除么?

=================

C# 和 Java 在一开始都是不支持泛型的。Java 在 1.5 开始才加入了泛型。为了让一个不支持泛型的语言支持泛型,只有两条路可以走:

  • 以前的非泛型容器保持不变,然后平行的增加一套泛型化的类型。

  • 直接把已有的非泛型容器扩展为泛型,不添加任何新的泛型版本。

Java 由于 1.5 之前市面上一句有大量的代码,所以不得以选择了第 2 种方式,而 C# 比较机智就选择了第一种。

而 Kotlin 本身就是基于 Java 1.6 编写的,一开始就有泛型,不存在兼容老版本代码的问题,那 Kotlin 实现的泛型还具备类型擦除么?

当然具备。上面其实已经说的很清楚了,Kotlin 本身就是基于 Java 1.6 编写的,而且 Kotlin 和 Java 有极强的互调能力,当然也存在类型擦除。

不过…

你还是会发现有意思的点:

val list = ArrayList()

// 报错:Not enough information to infer type variable E

在 Java 中,不指定泛型类型是没问题的,但 Kotlin 这样不好使了。想来也简单,毕竟在 Java 1.5 之前是肯定不存在上述类似代码的,而泛型的设计初衷就不是用来装默认的 Kotlin Any 的。

泛型的上界通配符

========

前面说到:因为 Java 的泛型本身具有「不可变性 Invariance」,所以即使 Fruit 类是 Apple 类的父类,但 Java 里面认为 List 和 List 类型并不一致,也就是说,子类的泛型 List 不属于泛型 List 的子类。

所以这样的代码并不被运行。

List apples = new ArrayList();

List fruits = apples;

// 多态用在这里会报错 Required type:List Provided: List

那假如我们想突破这层限制,怎么办?使用上界通配符 ? extends。

List apples = new ArrayList();

List<? extends Fruit> fruits = apples;

// 使用上界通配符后,编译不再报错

「上界通配符」,可以使 Java 泛型具有「协变性 Covariance」,协变就是允许上面的赋值是合法的。

在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。extends 限制了泛型类型的父类型,所以叫上界。

它有两层意思:

  • 其中 ? 是个通配符,表示这个 List 的泛型类型是一个未知类型。

  • extends 限制了这个未知类型的上界,也就是泛型类型必须满足这个 extends 的限制条件,这里和定义 class 的 extends 关键字有点不一样: 它的范围不仅是所有直接和间接子类,还包括上界定义的父类本身,也就是 Fruit。 它还有 implements 的意思,即这里的上界也可以是 interface。

这个突破限制有意义么?

有的有的。

假如我们有一个接口 Fruit:

interface Fruit {

float getWeight();

}

有两个水果类实现了 Fruit 接口:

class Banana implements Fruit {

@Override

public float getWeight() {

return 0.5f;

}

}

class Apple implements Fruit {

@Override

public float getWeight() {

return 1f;

}

}

假设我们有个需求是需要给水果称重:

List apples = new ArrayList<>();

apples.add(new Apple());

float totalWeight = getTotalWeight(apples);

// 报错:Required type: List Provided: List

private float getTotalWeight(List fruitList) {

float totalWeight = 0;

for (Fruit fruit : fruitList) {

totalWeight += fruit.getWeight();

}

return totalWeight;

}

想来这也是一个非常正常的需求,秤可以称各种水果的重量,但也可以只称苹果。你不能因为我只买苹果就不给我称重吧。所以把上面的代码加上上界通配符就可以啦。

List apples = new ArrayList<>();

apples.add(new Apple());

float totalWeight = getTotalWeight(apples);

// 不再报错

// 增加了上界通配符 ? extends

private float getTotalWeight(List<? extends Fruit> fruitList) {

float totalWeight = 0;

for (Fruit fruit : fruitList) {

totalWeight += fruit.getWeight();

}

return totalWeight;

}

不过,上面使用 ? extends 上界通配符突破了一层限制,却被施加了另一层限制:只可输出不可输入

什么意思呢?

比如:

List apples = new ArrayList();

List<? extends Fruit> fruits = apples;

Fruit fruit = fruits.get(0);

fruits.add(new Apple());

// 报错:Required type: capture of ? extends Fruit Provided: Apple

声明了上界通配符泛型的集合,不再允许 add 新的对象,Apple 不行,Fruit 也不行。拓展开来说:不止是集合,自己编写一个泛型做输入也不行

interface Shop {

void showFruitName(T t);

T getFruit();

}

Shop<? extends Fruit> apples = new Shop(){

@Override

public void showFruitName(Apple apple) { }

@Override

public Apple getFruit() {

return null;

}

};

apples.getFruit();

apples.showFruitName(new Apple());

// 报错:Required type: capture of ? extends Fruit Provided: Apple

泛型的下界通配符

========

泛型有上界通配符,那有没有下界通配符呢?

有的有的。

与上界通配符 ? extends 对应的就是下界通配符 ? super

下界通配符 ? super 所有情况和 ? extends 上界通配符刚刚相反:

  • 通配符 ? 表示 List 的泛型类型是一个 未知类型

  • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件 它的范围不仅是所有直接和间接子父类,还包括下界定义的子类本身。 super 同样支持 interface。

它被施加的新限制是:只可输入不可输出

Shop<? super Apple> apples = new Shop(){

@Override

public void showFruitName(Fruit apple) { }

@Override

public Fruit getFruit() {

return null;

}

};

apples.showFruitName(new Apple());

Apple apple = apples.getFruit();

// 报错:Required type: Apple Provided: capture of ? super Apple

解释下,首先 ? 表示未知类型,编译器是不确定它的类型的。

虽然不知道它的具体类型,不过在 Java 里任何对象都是 Object 的子类,所以这里只能把apples.getFruit() 获取出来的对象赋值给 Object。由于类型未知,所以直接赋值给一个 Apple 对象肯定是不负责任的,需要我们做一层强制转换,不过强制转换本身可能发生错误。

而 Apple 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 showFruitName 输入 Button 对象是合法的。

小结下,Java 的泛型本身是不支持协变和逆变的:

  • 可以使用泛型通配符 ? extends 来使泛型支持协变,但是「只能读取不能修改」,这里的修改仅指对泛型集合添加元素,如果是 remove(int index) 以及 clear 当然是可以的。

  • 可以使用泛型通配符 ? super 来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照 Object 读出来再强转当然也是可以的。

理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,就比较容易了。

Kotlin 的 out 和 in

=================

和 Java 泛型一样,Kolin 中的泛型本身也是不可变的。

不过换了一种表现形式:

  • 使用关键字 out 来支持协变,等同于 Java 中的上界通配符 ? extends。

  • 使用关键字 in 来支持逆变,等同于 Java 中的下界通配符 ? super。

val appleShop: Shop

val fruitShop: Shop

它们完全等价于:

Shop<? extends Fruit> appleShop;

Shop<? super Apple> fruitShop;

换了个写法,但作用是完全一样的。out 表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。

泛型的上下界约束

========

上面讲的都是在使用的时候再对泛型进行限制,我们称之为「上界通配符」和「下界通配符」。那我们可以在函数设计的时候,就设置这个限制么?

可以的可以的。

比如:

open class Animal

class PetShop<T : Animal?>(val t: T)

等同于 Java 的:

class PetShop {

private T t;

PetShop(T t) {

this.t = t;

}

}

这样,我们在设计宠物店类 PetShop 就给支持的泛型设置了上界约束,支持的泛型类型必须是 Animal 的之类。所以我们使用的话:

class Cat : Animal()

val catShop = PetShop(Cat())

val appleShop = PetShop(Apple())

// 报错:Type mismatch. Required: Animal? Found: Apple

很明显,Apple 并不是 Animal 的子类,当然不满足 PetShop 泛型类型的上界约束。

那…可以设置多个上界约束么?

当然可以,在 Java 中,给一个泛型参数声明多个约束的方式是,使用 &:

class PetShop<T extends Animal & Serializable> {

// 通过 & 实现了两个上界,必须是 Animal 和 Serializable 的子类或实现类

private T t;

PetShop(T t) {

this.t = t;

}

}

而在 Kotlin 中舍弃了 & 这种方式,而是增加了 where 关键字:

open class Animal

class PetShop(val t: T) where T : Animal?, T : Serializable

通过上面的方式,就实现了多个上界的约束。

Kotlin 的通配符 *

==============

前面我们说的泛型类型都是在我们需要知道参数类型是什么类型的,那如果我们对泛型参数的类型不感兴趣,有没有一种方式处理这个情况呢?

有的有的。

在 Kotlin 中,可以用通配符 * 来替代泛型参数。比如:

val list: MutableList<*> = mutableListOf(1, “nanchen2251”)

list.add(“nanchen2251”)

// 报错:Type mismatch. Required: Nothing Found: String

这个报错确实让人匪夷所思,上面用通配符代表了 MutableList 的泛型参数类型。初始化里面也加入了 String 类型,但在新 add 字符串的时候,却发生了编译错误。

而如果是这样的代码:

val list: MutableList = mutableListOf(1, “nanchen2251”)

list.add(“nanchen2251”)

// 不再报错

看来,所谓的通配符作为泛型参数并不等价于 Any 作为泛型参数。MutableList<*> 和 MutableList 并不是同一种列表,后者的类型是确定的,而前者的类型并不确定,编译器并不能知道这是一种什么类型。所以它不被允许添加元素,因为会导致类型不安全。

不过细心的同学肯定发现了,这个和前面泛型的协变非常类似。其实通配符 * 不过是一种语法糖,背后也是用协变来实现的。所以:MutableList<*> 等价于 MutableList<out Any?>,使用通配符与协变有着一样的特性。

在 Java 中,也有一样意义的通配符,不过使用的是 ? 作为通配。

List<?> list = new ArrayList();

Java 中的通配符 ? 也等价于 ? extends Object。

多个泛型参数声明

========

那可以声明多个泛型么?

可以的可以的。

HashMap 不就是一个典型的例子么?

class HashMap<K,V>

多个泛型,可以通过 , 进行分割,多个声明,上面是两个,实际上多个都是可以的。

class HashMap<K: Animal, V, T, M, Z : Serializable>

泛型方法

====

上面讲的都是都是在类上声明泛型类型,那可以声明在方法上么?

可以的可以的。

如果你是一名 Android 开发,View 的 findViewById 不就是最好的例子么?

public final T findViewById(@IdRes int id) {

if (id == NO_ID) {

return null;

}

return findViewTraversal(id);

}

最后

光有这些思路和搞懂单个知识的应用是还远远不够的,在Android开源框架设计思想中的知识点还是比较多的,想要搞懂还得学会整理和规划:我们常见的**Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:

image

这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。

由于文档内容过多,篇幅受限,只能截图展示部分

image

image

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!!!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
extends View> T findViewById(@IdRes int id) {

if (id == NO_ID) {

return null;

}

return findViewTraversal(id);

}

最后

光有这些思路和搞懂单个知识的应用是还远远不够的,在Android开源框架设计思想中的知识点还是比较多的,想要搞懂还得学会整理和规划:我们常见的**Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架,**这些都是属于Android开源框架设计思想的。如下图所示:

[外链图片转存中…(img-hgFguPgI-1715397104985)]

这位阿里P8大佬针对以上知识点,熬夜整理出了一本长达1042页的完整版如何解读开源框架设计思想PDF文档,内容详细,把Android热修复框架、插件化框架、组件化框架、图片加载框架、网络访问框架、RxJava响应式编程框架、IOC依赖注入框架、最近架构组件Jetpack等等Android第三方开源框架这些知识点从源码分析到实战应用都讲的简单明了。

由于文档内容过多,篇幅受限,只能截图展示部分

[外链图片转存中…(img-Jg90JKTr-1715397104986)]

[外链图片转存中…(img-w4GOBVqc-1715397104987)]

整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~

你的支持,我的动力;祝各位前程似锦,offer不断!!!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值