Java为什么不支持泛型数组
本文最后更新于:2023年7月15日 下午
前情提要
众所周知,Java
的泛型并不是真正的泛型
但是,究竟什么才是真正的泛型呢?
真正的泛型
根据New Bing
的说法:
真正的泛型是指泛型类型在编译时和运行时都能保持类型信息
真正的泛型的实现方式通常是类型膨胀,也就是为每个泛型参数生成一个单独的类或方法
这一点可以参考C++
的模板实现方式,在编译期为每一种使用的具体类型生成实例。
Java泛型
Java
的泛型并不是真正的泛型,而是伪泛型
采用了一种称为 类型擦除 的技术。类型擦除的意思是,在编译时,泛型类型会被替换为它的上界类型(如果没有指定上界,则为 Object
类型),并且在字节码层面,泛型信息会被擦除,不会保留到运行时
例如,我们定义了一个泛型类 Pair<T>
,其中 T
是一个无限定的类型变量,那么在编译后,它的原始类型就是 Pair<Object>
,也就是说 T 被替换为了 Object
类型。如果我们定义了一个泛型类 Pair<T extends Comparable>
,其中 T
是一个有限定的类型变量,那么在编译后,它的原始类型就是 Pair<Comparable>
,也就是说 T
被替换为了它的上界 Comparable
类型
那么,为什么 Java 要采用类型擦除的方式来实现泛型呢?这主要是出于 向后兼容性 的考虑。Java 泛型是在 Java 5 中引入的,而在此之前,Java 已经有了很多版本和很多类库。如果 Java 要实现真正的泛型,就需要修改 JVM 的指令集和类文件格式,这样就会导致之前的版本和类库无法在新版本的 JVM 上运行。为了避免这种情况,Java 设计者选择了一种不需要修改 JVM 的方式来实现泛型,也就是类型擦除。
类型擦除使得 Java 泛型能够与之前的版本和类库保持兼容,但也带来了一些限制和问题,比如:
- 不能使用基本类型作为泛型参数,只能使用包装类 // int × Integer √
- 不能根据泛型参数创建数组或实例化对象,只能使用反射或 Object 类型
- 不能对泛型参数进行 instanceof 检查或强制转换,只能使用通配符或边界
- 不能在静态方法或静态变量中使用泛型类的类型参数
- 不能创建泛型异常类
为什么不支持泛型数组
Java不能根据泛型参数创建数组,也不支持泛型数组
这俩看起来很像,但其实是两种不同的东东:
不能根据泛型参数创建数组
不能根据泛型参数创建数组,是指不能使用new T[size]
这样的语法来创建一个泛型类型的数组
因为在编译时无法确定T
的具体类型,也就找不到对应的类字节码文件
解决方案参考:Java中创建泛型数组 - minghai - 博客园
不支持泛型数组
不支持泛型数组,是指不能使用new ArrayList<T>[size]
这样的语法来创建一个泛型类型的数组
因为这样做会破坏类型安全,导致运行时的ClassCastException
异常
原因是Java的泛型是基于类型擦除实现的,在运行时泛型类型参数的具体类型信息是被擦除的,只剩下Object
类型
而Java的数组是协变的,也就是说子类数组可以向上转型为父类数组,例如String[]
可以转为Object[]
如果Java允许创建泛型数组,那么就可能出现如下情况:(New Bing提供)
1 |
|
为了避免这种情况发生,Java在编译时就禁止了创建泛型数组
所以并不是Java实现不了,而是出于安全禁止
What??
好的,想必以上的代码并没有触及根本,看了一脸懵逼
我们来仔细聊聊
首先看第三行:objArray[0] = new ArrayList<Integer>();
//Object[]可以存放任何对象
是没错,酱紫编译可以过,但是这和泛不泛型又有什么关系呢?
看以下两段代码:
1 |
|
这一段,把Float
赋值给Integer
数组arr
,编译器直接报错
1 |
|
这一段,把Float
赋值给Object
数组arr
,编译通过,但是运行抛出异常(类型不匹配)
可是可是,你有想过为什么吗,我凭什么知道编译时和运行时出不出错
静态类型 & 动态类型
Java在编译时只检查静态类型(变量类型[Left]),运行时才检查动态类型(真实类型 aka 引用指向的类型[Right])
例如:Object[] arr = new Integer[10];
中,arr
的静态类型是Object[]
,而动态类型是Integer[]
那么,有聪明的小可爱要问了,为什么不在编译时检查动态类型
好问题,因为编译时没法知道动态类型,看到动态俩字了嘛,我们可以根据用户输入改变引用的类型
1 |
|
所以编译器直接放弃检查动态类型,只判断静态类型
泛型数组
okayokay,继续
1 |
|
由于编译器类型擦除的存在,会变成:
1 |
|
编译器:诶,ArrayList
可以存入Object[]
,过!
虚拟机:诶,类型擦除,没类型,那就是ArrayList
存入ArrayList[]
,过!
1 |
|
由于strListArray
是List<String>[]
类型,可以取出一个String
但实际上strListArray[0]
存放的是一个ArrayList<Integer>
,所以会抛出ClassCastException
但是又有小可爱会说了:不是类型擦除了嘛,怎么还知道自己是ArrayList<Integer>
这是因为当我们从strListArray
中取出一个元素时,编译器会自动插入一个强制转换,将其转换为List<String>
类型。
结论:由于类型擦除的存在,编译器和虚拟机都没办法区分堆中的泛型数组到底存的是什么具体类型,而这对强类型语言Java来说是不可接受的,为了安全考虑,Java
直接不支持泛型数组了
以上内容参考:java为什么不支持泛型数组? - 红松的回答 - 知乎 https://www.zhihu.com/question/20928981/answer/2993376953
咋办
那泛型数组不能用咋办?
可以使用ArrayList
动态数组代替原生数组
因为ArrayList
不支持协变,所以也就安全得多(かも)
Conclusion
总而言之,言而总之
Java的类型擦除实现机制导致运行时失去了泛型信息,导致了很多限制
深究起来就阿巴阿巴了,反正大概就这个亚子,aaaaaaa,寄了再说
Ref
Java 不能实现真正泛型的原因是什么? - 知乎 (zhihu.com)
Java 中为什么不能创建泛型数组? - _路上 - 博客园 (cnblogs.com)