class Demo {
void func(Object t){
// …
}
}
可能你会好奇,在编译时发生类型擦除后,我们的泛型都被更换成了 Object,那为什么我们在使用的时候,却不需要强转操作呢?比如:
List list = new ArrayList<>();
list.add(“nanchen2251”);
String str = list.get(0);
// 这里并没有要求我们把 list.get(0) 强转为 String
这是因为编译器会根据我们声明的泛型类型进行提前的类型检查,然后再进行类型擦除,擦除为 Object,但在字节码中其实还存储了我们的泛型的类型信息,在使用到泛型类型的时候会把擦除后的 Object 自动做类型强转操作。所以上面的 list.get(0) 本身就是一个经过强转的 String 对象了。
这个技术看起来还蛮好的,但却有一个弊端。就是既然擦成 Object 了,那么在运行的时候,你根本不能确定这个对象到底是什么类型,虽然你可以通过编译器帮你插入的 checkcast 来获得此对象的类型。但是你并不能把 T 真正的当作一个类型使用:比如这条语句在 Java 中是非法的。
T a = new T();
// 报错:Type parameter ‘T’ cannot be instantiated directly
同理,因为都被擦成了 Object,你就不能根据类型来做某种区分。
比如 instanceof:
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 关键字:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://img-blog.csdnimg.cn/img_convert/8fe4a3a105900cda4b4ad1c0c2f634e5.jpeg)
最后
Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。
人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。
资源持续更新中,欢迎大家一起学习和探讨。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
**
[外链图片转存中…(img-k3OEfLAJ-1712069783400)]
[外链图片转存中…(img-RzsKf11n-1712069783401)]
[外链图片转存中…(img-7aoWpSU8-1712069783401)]
[外链图片转存中…(img-ozGtj15B-1712069783401)]
[外链图片转存中…(img-4NDj9TSe-1712069783401)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://img-blog.csdnimg.cn/img_convert/8fe4a3a105900cda4b4ad1c0c2f634e5.jpeg)
最后
Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。
人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。
资源持续更新中,欢迎大家一起学习和探讨。