一文读懂 Java 和 Kotlin 的泛型难点(1)
学完之后,若是想验收效果如何,其实最好的方法就是可自己去总结一下。比如我就会在学习完一个东西之后自己去手绘一份xmind文件的知识梳理大纲脑图,这样也可方便后续的复习,且都是自己的理解,相信随便瞟几眼就能迅速过完整个知识,脑补回来。下方即为我手绘的MyBtis知识脑图,由于是xmind文件,不好上传,所以小编将其以图片形式导出来传在此处,细节方面不是特别清晰。但可给感兴趣的朋友提供完整的MyBti
this.obj = obj;
}
}
public static void main(String[] args) {
NodeA nodeA = new NodeA(“业志陈”);
NodeB nodeB = new NodeB<>(“业志陈”);
System.out.println(nodeB.obj);
}
}
复制代码
可以看到 NodeA 和 NodeB 两个对象对应的字节码其实是完全一样的,最终都是使用 Object 来承载数据,就好像传递给 NodeB 的类型参数 String 不见了一样,这便是类型擦除
public class generic.GenericTest {
public generic.GenericTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.“”😦)V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class generic/GenericTest$NodeA
3: dup
4: ldc #3 // String 业志陈
6: invokespecial #4 // Method generic/GenericTest$NodeA.“”:(Ljava/lang/Object;)V
9: astore_1
10: new #5 // class generic/GenericTest$NodeB
13: dup
14: ldc #3 // String 业志陈
16: invokespecial #6 // Method generic/GenericTest$NodeB.“”:(Ljava/lang/Object;)V
19: astore_2
20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_2
24: invokestatic #8 // Method generic/GenericTest$NodeB.access 000 : ( L g e n e r i c / G e n e r i c T e s t 000:(Lgeneric/GenericTest 000:(Lgeneric/GenericTestNodeB;)Ljava/lang/Object;
27: checkcast #9 // class java/lang/String
30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
}
复制代码
而如果让 NodeA 直接使用 String 类型,并且为泛型类 NodeB 设定上界约束 String,两者的字节码也会完全一样
public class GenericTest {
public static class NodeA {
private String obj;
public NodeA(String obj) {
this.obj = obj;
}
}
public static class NodeB {
private T obj;
public NodeB(T obj) {
this.obj = obj;
}
}
public static void main(String[] args) {
NodeA nodeA = new NodeA(“业志陈”);
NodeB nodeB = new NodeB<>(“业志陈”);
System.out.println(nodeB.obj);
}
}
复制代码
可以看到 NodeA 和 NodeB 的字节码是完全相同的
public class generic.GenericTest {
public generic.GenericTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object.“”😦)V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class generic/GenericTest$NodeA
3: dup
4: ldc #3 // String 业志陈
6: invokespecial #4 // Method generic/GenericTest$NodeA.“”:(Ljava/lang/String;)V
9: astore_1
10: new #5 // class generic/GenericTest$NodeB
13: dup
14: ldc #3 // String 业志陈
16: invokespecial #6 // Method generic/GenericTest$NodeB.“”:(Ljava/lang/String;)V
19: astore_2
20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_2
24: invokestatic #8 // Method generic/GenericTest$NodeB.access 000 : ( L g e n e r i c / G e n e r i c T e s t 000:(Lgeneric/GenericTest 000:(Lgeneric/GenericTestNodeB;)Ljava/lang/String;
27: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: return
}
复制代码
所以说,当泛型类型被擦除后有两种转换方式
-
如果泛型没有设置上界约束,那么将泛型转化成 Object 类型
-
如果泛型设置了上界约束,那么将泛型转化成该上界约束
该结论也可以通过反射泛型类的 Class 对象来验证
public class GenericTest {
public static class NodeA {
private T obj;
public NodeA(T obj) {
this.obj = obj;
}
}
public static class NodeB {
private T obj;
public NodeB(T obj) {
this.obj = obj;
}
}
public static void main(String[] args) {
NodeA nodeA = new NodeA<>(“业志陈”);
getField(nodeA.getClass());
NodeB nodeB = new NodeB<>(“https://juejin.cn/user/923245496518439”);
getField(nodeB.getClass());
}
private static void getField(Class clazz) {
for (Field field : clazz.getDeclaredFields()) {
System.out.println("fieldName: " + field.getName());
System.out.println("fieldTypeName: " + field.getType().getName());
}
}
}
复制代码
NodeA 对应的是 Object,NodeB 对应的是 String
fieldName: obj
fieldTypeName: java.lang.Object
fieldName: obj
fieldTypeName: java.lang.String
复制代码
那既然在运行时不存在任何类型相关的信息,泛型又为什么能够实现类型检查和类型自动转换等功能呢?
其实,类型检查是编译器在编译前帮我们完成的,编译器知道我们声明的具体的类型实参,所以类型擦除并不影响类型检查功能。而类型自动转换其实是通过内部强制类型转换来实现的,上面给出的字节码中也可以看到有一条类型强转 checkcast 的语句
27: checkcast #9 // class java/lang/String
复制代码
例如,ArrayList 内部虽然用于存储数据的是 Object 数组,但 get 方法内部会自动完成类型强转
transient Object[] elementData;
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings(“unchecked”)
E elementData(int index) {
//强制类型转换
return (E) elementData[index];
}
复制代码
所以 Java 的泛型可以看做是一种特殊的语法糖,因此也被人称为伪泛型
四、类型擦除的后遗症
Java 泛型对于类型的约束只在编译期存在,运行时仍然会按照 Java 5 之前的机制来运行,泛型的具体类型在运行时已经被删除了,所以 JVM 是识别不到我们在代码中指定的具体的泛型类型的
例如,虽然List<String>
只能用于添加字符串,但我们只能泛化地识别到它属于List<?>
类型,而无法具体判断出该 List 内部包含的具体类型
List stringList = new ArrayList<>();
//正常
if (stringList instanceof ArrayList<?>) {
}
//报错
if (stringList instanceof ArrayList) {
}
复制代码
我们只能对具体的对象实例进行类型校验,但无法判断出泛型形参的具体类型
public void filter(T data) {
//正常
if (data instanceof String) {
}
//报错
if (T instanceof String) {
}
//报错
Class tClass = T::getClass;
}
复制代码
此外,类型擦除也会导致 Java 中出现多态问题。例如,以下两个方法的方法签名并不完全相同,但由于类型擦除的原因,入参参数的数据类型都会被看成 List<Object>
,从而导致两者无法共存在同一个区域内
public void filter(List stringList) {
}
public void filter(List stringList) {
}
复制代码
五、Kotlin 泛型
Kotlin 泛型在大体上和 Java 一致,毕竟两者需要保证兼容性
class Plate(val t: T) {
fun cut() {
println(t.toString())
}
}
class Apple
class Banana
fun main() {
val plateApple = Plate(Apple())
//泛型类型自动推导
val plateBanana = Plate(Banana())
plateApple.cut()
plateBanana.cut()
}
复制代码
Kotlin 也支持在扩展函数中使用泛型
fun List.find(t: T): T? {
val index = indexOf(t)
return if (index > -1) get(index) else null
}
复制代码
需要注意的是,为了实现向后兼容,目前高版本 Java 依然允许实例化没有具体类型参数的泛型类,这可以说是一个对新版本 JDK 危险但对旧版本友好的兼容措施。但 Kotlin 要求在使用泛型时需要显式声明泛型类型或者是编译器能够类型推导出具体类型,任何不具备具体泛型类型的泛型类都无法被实例化。因为 Kotlin 一开始就是基于 Java 6 版本的,一开始就存在了泛型,自然就不存在需要兼容老代码的问题,因此以下例子和 Java 会有不同的表现
val arrayList1 = ArrayList() //错误,编译器报错
val arrayList2 = arrayListOf() //正常
val arrayList3 = arrayListOf(1, 2, 3) //正常
复制代码
还有一个比较容易让人误解的点。我们经常会使用 as
和 as?
来进行类型转换,但如果转换对象是泛型类型的话,那就会由于类型擦除而出现误判。如果转换对象有正确的基础类型,那么转换就会成功,而不管类型实参是否相符。因为在运行时转换发生的时候类型实参是未知的,此时编译器只会发出 “unchecked cast” 警告,代码还是可以正常编译的
例如,在以下例子中代码的运行结果还符合我们的预知。第一个转换操作由于类型相符,所以打印出了相加值。第二个转换操作由于基础类型是 Set 而非 List,所以抛出了 IllegalAccessException
fun main() {
printSum(listOf(1, 2, 3)) //6
printSum(setOf(1, 2, 3)) //IllegalAccessException
}
fun printSum(c: Collection<*>) {
val intList = c as? List ?: throw IllegalAccessException(“List is expected”)
println(intList.sum())
}
复制代码
而在以下例子中抛出的却是 ClassCastException,这是因为在运行时不会判断且无法判断出类型实参到底是否是 Int,而只会判断基础类型 List 是否相符,所以 as?
操作会成功,等到要执行相加操作时才会发现拿到的是 String 而非 Number
printSum(listOf(“1”, “2”, “3”))
Exception in thread “main” java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
复制代码
六、上界约束
泛型本身已经带有类型约束的作用,我们也可以进一步细化其支持的具体类型
例如,假设存在一个盘子 Plate,我们要求该 Plate 只能用于装水果 Fruit,那么就可以对其泛型声明做进一步约束,Java 中使用 extend 关键字来声明约束规则,而 Kotlin 使用的是 : 。这样 Plate 就只能用于 Fruit 和其子类,而无法用于 Noodles 等不相关的类型,这种类型约束就被称为上界约束
open class Fruit
class Apple : Fruit()
class Noodles
class Plate(val t: T)
fun main() {
val applePlate = Plate(Apple()) //正常
val noodlesPlate = Plate(Noodles()) //报错
}
复制代码
如果上界约束拥有多层类型元素,Java 是使用 & 符号进行链式声明,Kotlin 则是用 where 关键字来依次进行声明
interface Soft
class Plate(val t: T) where T : Fruit, T : Soft
open class Fruit
class Apple : Fruit()
class Banana : Fruit(), Soft
fun main() {
val applePlate = Plate(Apple()) //报错
val bananaPlate = Plate(Banana()) //正常
}
复制代码
此外,没有指定上界约束的类型形参会默认使用 Any? 作为上界,即我们可以使用 String 或 String? 作为具体的类型实参。如果想确保最终的类型实参一定是非空类型,那么就需要主动声明上界约束为 Any
七、类型通配符 & 星号投影
假设现在有个需求,需要我们提供一个方法用于遍历所有类型的 List 集合并打印元素
第一种做法就是直接将方法参数类型声明为 List,不包含任何泛型类型声明。这种做法可行,但编译器会警告无法确定 list
元素的具体类型,所以这不是最优解法
public static void printList1(List list) {
for (Object o : list) {
System.out.println(o);
}
}
复制代码
可能会想到的第二种做法是:将泛型类型直接声明为 Object,希望让其适用于任何类型的 List。这种做法完全不可行,因为即使 String
是 Object
的子类,但 List<String>
和 List<Object>
并不具备从属关系,这导致 printList2
方法实际上只能用于List<Object>
这一种具体类型
public static void printList2(List list) {
for (Object o : list) {
System.out.println(o);
}
}
复制代码
最优解法就是要用到 Java 的类型通配符 ? 了,printList3
方法完全可行且编译器也不会警告报错
public static void printList3(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
复制代码
? 表示我们并不关心具体的泛型类型,而只是想配合其它类型进行一些条件限制。例如,printList3
方法希望传入的是一个 List,但不限制泛型的具体类型,此时List<?>
就达到了这一层限制条件
类型通配符也存在着一些限制。因为 printList3
方法并不包含具体的泛型类型,所以我们从中取出的值只能是 Object 类型,且无法向其插入值,这都是为了避免发生 ClassCastException
Java 的类型通配符对应 Kotlin 中的概念就是**星号投影 * **,Java 存在的限制在 Kotlin 中一样有
fun printList(list: List<*>) {
for (any in list) {
println(any)
}
}
复制代码
此外,星号投影只能出现在类型形参的位置,不能作为类型实参
val list: MutableList<*> = ArrayList() //正常
val list2: MutableList<> = ArrayList<>() //报错
复制代码
八、协变 & 不变
看以下例子。Apple 和 Banana 都是 Fruit 的子类,可以发现 Apple[] 类型的对象是可以赋值给 Fruit[] 的,且 Fruit[] 可以容纳 Apple 对象和 Banana 对象,这种设计就被称为协变,即如果 A 是 B 的子类,那么 A[] 就是 B[] 的子类型。相对的,Object[] 就是所有数组对象的父类型
static class Fruit {
}
static class Apple extends Fruit {
}
static class Banana extends Fruit {
}
public static void main(String[] args) {
Fruit[] fruitArray = new Apple[10];
//正常
fruitArray[0] = new Apple();
//编译时正常,运行时抛出 ArrayStoreException
fruitArray[1] = new Banana();
}
复制代码
而 Java 中的泛型是不变的,这意味着 String 虽然是 Object 的子类,但List<String>
并不是List<Object>
的子类型,两者并不具备继承关系
List stringList = new ArrayList<>();
List objectList = stringList; //报错
复制代码
那为什么 Java 中的泛型是不变的呢?
这可以通过看一个例子来解释。假设 Java 中的泛型是协变的,那么以下代码就可以成功通过编译阶段的检查,在运行时就不可避免地将抛出 ClassCastException,而引入泛型的初衷就是为了实现类型安全,支持协变的话那泛型也就没有比数组安全多少了,因此就将泛型被设计为不变的
List strList = new ArrayList<>();
List objs = strList; //假设可以运行,实际上编译器会报错
objs.add(1);
String str = strList.get(0); //将抛出 ClassCastException,无法将整数转换为字符串
复制代码
再来想个问题,既然协变本身并不安全,那么数组为何又要被设计为协变呢?
Arrays 类包含一个 equals
方法用于比较两个数组对象是否相等。如果数组是协变的,那么就需要为每一种数组对象都定义一个 equals
方法,包括开发者自定义的数据类型。想要避免这种情况,就需要让 Object[] 可以接收任意数组类型,即让 Object[] 成为所有数组对象的父类型,这就使得数组必须支持协变,这样多态才能生效
public class Arrays {
public static boolean equals(Object[] a, Object[] a2) {
if (a==a2)
return true;
if (anull || a2null)
return false;
int length = a.length;
if (a2.length != length)
return false;
for (int i=0; i<length; i++) {
Object o1 = a[i];
Object o2 = a2[i];
if (!(o1null ? o2null : o1.equals(o2)))
return false;
}
return true;
}
}
复制代码
需要注意的是,Kotlin 中的数组和 Java 中的数组并不一样,Kotlin 数组并不支持协变,Kotlin 数组类似于集合框架,具有对应的实现类 Array,Array 属于泛型类,支持了泛型因此也不再协变
val stringArray = arrayOfNulls(3)
val anyArray: Array<Any?> = stringArray //报错
复制代码
Java 的泛型也并非完全不变的,只是实现协变需要满足一些条件,甚至也可以实现逆变,下面就来介绍下泛型如何实现协变和逆变
九、泛型协变
假设我们定义了一个copyAll
希望用于 List 数据迁移。那以下操作在我们看来就是完全安全的,因为 Integer 是 Number 的子类,按道理来说是能够将 Integer 保存为 Number 的,但由于泛型不变性,List<Integer>
并不是List<Number>
的子类型,所以实际上该操作将报错
public static void main(String[] args) {
List numberList = new ArrayList<>();
List integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);
copyAll(numberList, integerList); //报错
}
private static void copyAll(List to, List from) {
to.addAll(from);
}
复制代码
思考下该操作为什么会报错?
编译器的作用之一就是进行安全检查并阻止可能发生不安全行为的操作,copyAll
方法会报错,那么肯定就是编译器觉得该方法有可能会触发不安全的操作。开发者的本意是希望将 Integer 类型的数据转移到 NumberList 中,只有这种操作且这种操作在我们看来肯定是安全的,但是编译器不知道开发者最终所要做的具体操作啊
假设 copyAll
方法可以正常调用,那么copyAll
方法自然只会把 from
当做 List<Number>
来看待。因为 Integer 是 Number 的子类,从 integerList
获取到的数据对于 numberList
来说自然是安全的。而如果我们在copyAll
方法中偷偷向 integerList
传入了一个 Number 类型的值的话,那么自然就将抛出异常,因为 from 实际上是 List<Integer>
类型
为了阻止这种不安全的行为,编译器选择通过直接报错来进行提示。为了解决报错,我们就需要向编译器做出安全保证:从 from 取出来的值只会当做 Number 类型,且不会向 from 传入任何值
为了达成以上保证,需要修改下 copyAll
方法
private static void copyAll(List to, List<? extends T> from) {
to.addAll(from);
}
复制代码
? extends T
表示 from
接受 T 或者 T 的子类型,而不单单是 T 自身,这意味着我们可以安全地从 from
中取值并声明为 T 类型,但由于我们并不知道 T 代表的具体类型,写入操作并不安全,因此编译器会阻止我们向 from
执行传值操作。有了该限制后,从integerList
中取出来的值只能是当做 Number 类型,且避免了向integerList
插入非法值的可能,此时List<Integer>
就相当于List<? extends Number>
的子类型了,从而使得 copyAll
方法可以正常使用
简而言之,带 extends 限定了上界的通配符类型使得泛型参数类型是协变的,即如果 A 是 B 的子类,那么 Generic<A>
就是Generic<? extends B>
的子类型
十、泛型逆变
协变所能做到的是:如果 A 是 B 的子类,那么 Generic<A>
就是Generic<? extends B>
的子类型。逆变相反,其代表的是:如果 A 是 B 的子类,那么 Generic<B>
就是 Generic<? super A>
的子类型
协变还比较好理解,毕竟其继承关系是相同的,但逆变就比较反直觉了,整个继承关系都倒过来了
逆变的作用可以通过相同的例子来理解,copyAll
方法如下修改也可以正常使用,此时就是向编译器做出了另一种安全保证:向 numberList 传递的值只会是 Integer 类型,且从 numberList 取出的值也只会当做 Object 类型
private static void copyAll(List<? super T> to, List from) {
to.addAll(from);
}
复制代码
? super T
表示 to
接收 T 或者 T 的父类型,而不单单是 T 自身,这意味着我们可以安全地向 to
传类型为 T 的值,但由于我们并不知道 T 代表的具体类型,所以从 to
取出来的值只能是 Object 类型。有了该限制后,integerList
只能向 numberList
传递类型为 Integer 的值,且避免了从 numberList
中获取到非法类型值的可能,此时List<Number>
就相当于List<? super Integer>
的子类型了,从而使得 copyAll
方法可以正常使用
简而言之,带 super 限定了下界的通配符类型使得泛型参数类型是逆变的,即如果 A 是 B 的子类,那么 Generic<B>
就是 Generic<? super A>
的子类型
十一、out & in
Java 中关于泛型的困境在 Kotlin 中一样存在,out 和 in 都是 Kotlin 的关键字,其作用都是为了来应对泛型问题。in
和 out
是一个对立面,同时它们又与泛型不变相对立,统称为型变
-
out 本身带有出去的意思,本身带有倾向于取值操作的意思,用于泛型协变
-
in 本身带有进来的意思,本身带有倾向于传值操作的意思,用于泛型逆变
再来看下相同例子,该例子在 Java 中存在的问题在 Kotlin 中一样有
fun main() {
val numberList = mutableListOf()
val intList = mutableListOf(1, 2, 3, 4)
copyAll(numberList, intList) //报错
numberList.forEach {
println(it)
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后:学习总结——MyBtis知识脑图(纯手绘xmind文档)
学完之后,若是想验收效果如何,其实最好的方法就是可自己去总结一下。比如我就会在学习完一个东西之后自己去手绘一份xmind文件的知识梳理大纲脑图,这样也可方便后续的复习,且都是自己的理解,相信随便瞟几眼就能迅速过完整个知识,脑补回来。下方即为我手绘的MyBtis知识脑图,由于是xmind文件,不好上传,所以小编将其以图片形式导出来传在此处,细节方面不是特别清晰。但可给感兴趣的朋友提供完整的MyBtis知识脑图原件(包括上方的面试解析xmind文档)
除此之外,前文所提及的Alibaba珍藏版mybatis手写文档以及一本小小的MyBatis源码分析文档——《MyBatis源码分析》等等相关的学习笔记文档,也皆可分享给认可的朋友!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
berList, intList) //报错
numberList.forEach {
println(it)
}
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-oelfF4f0-1713564539136)]
[外链图片转存中…(img-POm0rbBh-1713564539141)]
[外链图片转存中…(img-rOi2TXFx-1713564539143)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后:学习总结——MyBtis知识脑图(纯手绘xmind文档)
学完之后,若是想验收效果如何,其实最好的方法就是可自己去总结一下。比如我就会在学习完一个东西之后自己去手绘一份xmind文件的知识梳理大纲脑图,这样也可方便后续的复习,且都是自己的理解,相信随便瞟几眼就能迅速过完整个知识,脑补回来。下方即为我手绘的MyBtis知识脑图,由于是xmind文件,不好上传,所以小编将其以图片形式导出来传在此处,细节方面不是特别清晰。但可给感兴趣的朋友提供完整的MyBtis知识脑图原件(包括上方的面试解析xmind文档)
[外链图片转存中…(img-w6zsSski-1713564539144)]
除此之外,前文所提及的Alibaba珍藏版mybatis手写文档以及一本小小的MyBatis源码分析文档——《MyBatis源码分析》等等相关的学习笔记文档,也皆可分享给认可的朋友!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
更多推荐
所有评论(0)