在Java的类型系统中,数组有什么缺陷吗?
2020年2月,王垠吐槽了下Java的类型系统,说:
关于程序员对 Java 类型系统的理解,比较高级的一个面试问题是这样:
1 | public static void f() { |
这段代码里面到底哪一行错了?为什么?如果某个 Java 版本能顺利运行这段代码,那么如何让这个错误暴露得更致命一些?
注意这里所谓的「错了」是本质上,原理上的。
那么这儿的“错误”是指什么呢?
TL;DR
如果只能用一句话回答这个问题的话,那么就是:
Java数组不支持泛型,破坏了Java的类型安全性
类型系统的一些前提
一个好的类型系统,能够尽可能早的检测出错误,比如你将一个String赋值给int变量的时候,编译器就会报错,而不是等程序跑起来再报错。
Java的数组设计坏在哪儿
为了表述简单,我们假设Java支持了范型数组哈,比如<?>[]
这样的表示法。
1 | public static void f() { |
上面这段代码,在第二步的时候,其实就出现了一丝不对劲。将一个String[]
转化成一个Object[]
,这导致了数组的类型细节“逃逸”除了类型系统。
或者用更加明白的话来说:在第四步,给一个String[]
里面塞一个Integer对象的时候,编译器就应该报错。
如果能够重来,应该怎么设计
如果按照完美的类型系统来设计,王垠的代码应该是这个样子的:
1 | // 我们依然假设Java支持了范型数组 |
这个程序这回看起来正常了很多,而且根据Java范型的规则,在第二步也能顺利触发编译失败。<String>[]
转换成<? super String>[]
,这当然不能成功,要不然后面把Integer对象往里塞的时候,类型系统就没法判断了。
问题还没有结束
把<String>[]
转换成<? super String>[]
,本质是为了读取:可以把String当作Object来读取。
而上面的例子,实现的是写入。难道范型数组,就没法支持读取了吗?
当然不。范型的上下界就是用来做这些限定的,示例代码如下:
1 | public static void f() { |
- 类型参数中通过
<? super T>
限制了下界,那么写入就不会有问题,总是可以按照T类型往里面写,但读取变得不太可能。 - 类型参数中通过
<? extens T>
限制了上界,那么读取就不会有问题,总是可以按照T类型读取,但写入变得不太可能。
有没有更简单的表述呢?
在类型系统中,List和array是类似的,正好Java的List支持了范型,那么我们用List重写上面的例子:
1 | public static void f() { |
可以看到,上面用List的程序,用类型系统+范型的上下界,很完美的限制了类型不安全的操作。
但是,由于数组array不支持范型,导致JVM在实现的时候,只能将数组处理成协变的,允许了类型不安全的转换操作,导致了Java类型系统的“漏洞”。
本文整理自我的知乎回答。
在Java的类型系统中,数组有什么缺陷吗?