Java 类型系统的 BUG
最近看到一道比较有意思的 Java 面试题,在这里给大家分享一下,题目如下:
1 | String[] a = new String[2]; |
那么问题来了:以上 4 行代码有什么问题?
当我看到这道题的时候,第一印象是:第 2 行有问题
在 Java 中,数组是没有继承关系的,
String[]
在 JVM 中的表示是[Ljava/lang/String;
,Object[]
在 JVM 中的表示是[Ljava/lang/Object;
,它们的父类都是java.lang.Object
。
Java 的表现
当我把代码输入到 IDE 中时,发现并没有语法错误,我想这难道是 IDE 的 bug ?,于是又打开了 VIM,写了个测试类:
1 | public class Main { |
手动 javac 编译,发现也能编译过,这就有意思了,当我反编译 class 文件后,发现竟然是这样的:
1 | public static void main(String[] var0) { |
为了确认反编译的结果,于是又用 javap 输出了 main(String[])
方法的字节码,如下所示:
1 | 0: iconst_2 |
看起来 javac 对 main(String[]) 方法做了优化,把局部变量 Object[] b
给去掉了,大家猜猜这段代码运行的结果是什么?
可能有人会想,难道是 ClassCastException
?可是看字节码,哪里有 checkcast
指令?
运行一下试试呗,这就是实际的结果:
1 | Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer |
居然还有 ArrayStoreException
这样的异常,再看看 ArrayStoreException
的源码:
1 | /** |
注释里已经写得很清楚了,可是,明明知道这么写是有问题的,为什么还要让它发生呢?就像 ArithmeticException
一样,明知道可能会发生异常,为什么 IDE 也不提示一下呢?关键是,为什么将 String[]
赋值给 Object[]
会被认为是正确的呢?
带着疑问,翻了一下 JVM 规范,原来 aastore 指令已经给出了给出了明确的定义:
1 | aastore value, index, arrayref |
在运行时,value
的类型必须与 arrayref
引用的数组组件的类型兼容。具体来说,仅在以下情况下才允许将引用类型 S
(源)的值赋给引用类型 T
(目标)的数组组件:
- 如果
S
是Class
类型,则:- 如果
T
是Class
类型,则S
必须与T
属于同一类,或者S
必须是 `T 的子类; - 如果
T
是接口类型,则S
必须实现接口T
。
- 如果
- 如果
S
是接口类型,则:- 如果
T
是Class
类型,则T
必须是Object
。 - 如果
T
是接口类型,则T
必须是与S
相同的接口或S
的超接口。
- 如果
- 如果
S
是数组类型SC[]
,即数组组件为SC
类型,则:- 如果
T
是Class
类型,则T
必须是Object
。 - 如果
T
是接口类型,则T
必须是由数组实现的接口之一(JLS§4.10.3)。 - 如果
T
为数组类型TC[]
,即数组组件为TC
类型,则以下条件之一必须为true
:TC
和SC
是相同的原始类型。TC
和SC
是引用类型,类型SC
可赋给TC
。
- 如果
所以,根据上面的最后一条规则,String[]
是可以赋给 Object[]
,因为:
String[]
和Object[]
都为引用类型String
可以赋给Object
既然这样,那么,下面这段代码的结果是怎样的呢?
1 | public static void main(String[] args) { |
运行一下,结果为:
1 | class java.lang.Object |
这说明了什么问题:
- 数组不存在继承关系
- 不存在继承关系的两种不同的引用类型之间可以赋值(这难道不是与多态的定义相违悖?)
Kotlin 的表现
如果把上面的代码翻译成 Kotlin,会不会有不一样的结果呢?
看来 Kotlin 的类型系统相对于 Java 来说更严谨。
- 本文链接:https://johnsonlee.io/2020/05/12/a-bug-of-java-type-system/
- 版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。