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
2
3
4
5
List<String>[] strListArray = new ArrayList<String>[10]; //假设这样可以创建泛型数组
Object[] objArray = strListArray; //由于数组协变,可以向上转型为Object[]
objArray[0] = new ArrayList<Integer>(); //由于Object[]可以存放任何对象,可以放入一个ArrayList<Integer>
String str = strListArray[0].get(0); //由于strListArray是List<String>[]类型,可以取出一个String
//但实际上strListArray[0]存放的是一个ArrayList<Integer>,所以会抛出ClassCastException

为了避免这种情况发生,Java在编译时就禁止了创建泛型数组

所以并不是Java实现不了,而是出于安全禁止

What??

好的,想必以上的代码并没有触及根本,看了一脸懵逼

我们来仔细聊聊

首先看第三行:objArray[0] = new ArrayList<Integer>(); //Object[]可以存放任何对象

是没错,酱紫编译可以过,但是这和泛不泛型又有什么关系呢?

看以下两段代码:

1
2
Integer[] arr = new Integer[10];
arr[0] = new Float(10);

这一段,把Float赋值给Integer数组arr,编译器直接报错

1
2
Object[] arr = new Integer[10];
arr[0] = new Float(10);

这一段,把Float赋值给Object数组arr,编译通过,但是运行抛出异常(类型不匹配)

可是可是,你有想过为什么吗,我凭什么知道编译时和运行时出不出错

静态类型 & 动态类型

Java在编译时只检查静态类型(变量类型[Left]),运行时才检查动态类型(真实类型 aka 引用指向的类型[Right])

例如:Object[] arr = new Integer[10];中,arr的静态类型是Object[],而动态类型是Integer[]

那么,有聪明的小可爱要问了,为什么不在编译时检查动态类型

好问题,因为编译时没法知道动态类型,看到动态俩字了嘛,我们可以根据用户输入改变引用的类型

1
2
3
4
5
6
Animal animal = null; //声明一个Animal类型的变量
int choice = Integer.parseInt(args[0]); //从命令行参数获取一个整数
if (choice)
animal = new Dog(); //如果输入为1,创建一个Dog对象并赋值给animal
else
animal = new Animal(); //否则,创建一个Animal对象并赋值给animal

所以编译器直接放弃检查动态类型,只判断静态类型

泛型数组

okayokay,继续

1
2
3
List<String>[] strListArray = new ArrayList<String>[10];
Object[] objArray = strListArray;
objArray[0] = new ArrayList<Integer>();

由于编译器类型擦除的存在,会变成:

1
2
3
List<String>[] strListArray = new ArrayList[10];
Object[] objArray = strListArray;
objArray[0] = new ArrayList();

编译器:诶,ArrayList可以存入Object[],过!

虚拟机:诶,类型擦除,没类型,那就是ArrayList存入ArrayList[],过!

1
String str = strListArray[0].get(0); 

由于strListArrayList<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为什么不支持泛型数组? - 知乎

Java 不能实现真正泛型的原因是什么? - 知乎 (zhihu.com)

Java 中为什么不能创建泛型数组? - _路上 - 博客园 (cnblogs.com)

Java 泛型的本质——类型擦除_anlian523的博客-CSDN博客

Java中创建泛型数组 - minghai - 博客园 (cnblogs.com)


Java为什么不支持泛型数组
https://mrbeancpp.github.io/2023/07/15/Java为什么不支持泛型数组/
作者
MrBeanC
发布于
2023年7月15日
许可协议