最近看到一道比较有意思的 Java 面试题,在这里给大家分享一下,题目如下:

1
2
3
4
String[] a = new String[2];
Object[] b = a;
a[0] = "hello";
b[1] = Integer.valueOf(0);

那么问题来了:以上 4 行代码有什么问题?

当我看到这道题的时候,第一印象是:第 2 行有问题

Java 中,数组是没有继承关系的,String[]JVM 中的表示是 [Ljava/lang/String;Object[]JVM 中的表示是 [Ljava/lang/Object;,它们的父类都是 java.lang.Object

Java 的表现

当我把代码输入到 IDE 中时,发现并没有语法错误,我想这难道是 IDEbug ?,于是又打开了 VIM,写了个测试类:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String[] a = new String[2];
Object[] b = a;
a[0] = "hello";
b[1] = Integer.valueOf(0);
System.out.println(a);
}
}

手动 javac 编译,发现也能编译过,这就有意思了,当我反编译 class 文件后,发现竟然是这样的:

1
2
3
4
public static void main(String[] var0) {
String[] var1 = new String[]{"hello", 0};
System.out.println(var1);
}

为了确认反编译的结果,于是又用 javap 输出了 main(String[]) 方法的字节码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 0: iconst_2
1: anewarray #2 // class java/lang/String
4: astore_1
5: aload_1
6: astore_2
7: aload_1
8: iconst_0
9: ldc #3 // String hello
11: aastore
12: aload_2
13: iconst_1
14: iconst_0
15: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
18: aastore
19: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
22: aload_1
23: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
26: return

看起来 javacmain(String[]) 方法做了优化,把局部变量 Object[] b 给去掉了,大家猜猜这段代码运行的结果是什么?

可能有人会想,难道是 ClassCastException?可是看字节码,哪里有 checkcast 指令?

运行一下试试呗,这就是实际的结果:

1
2
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
at Main.main(Main.java:6)

居然还有 ArrayStoreException 这样的异常,再看看 ArrayStoreException 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Thrown to indicate that an attempt has been made to store the
* wrong type of object into an array of objects. For example, the
* following code generates an <code>ArrayStoreException</code>:
* <blockquote><pre>
* Object x[] = new String[3];
* x[0] = new Integer(0);
* </pre></blockquote>
*
* @author unascribed
* @since JDK1.0
*/
public
class ArrayStoreException extends RuntimeException {
private static final long serialVersionUID = -4522193890499838241L;

/**
* Constructs an <code>ArrayStoreException</code> with no detail message.
*/
public ArrayStoreException() {
super();
}

/**
* Constructs an <code>ArrayStoreException</code> with the specified
* detail message.
*
* @param s the detail message.
*/
public ArrayStoreException(String s) {
super(s);
}
}

注释里已经写得很清楚了,可是,明明知道这么写是有问题的,为什么还要让它发生呢?就像 ArithmeticException 一样,明知道可能会发生异常,为什么 IDE 也不提示一下呢?关键是,为什么将 String[] 赋值给 Object[] 会被认为是正确的呢?

带着疑问,翻了一下 JVM 规范,原来 aastore 指令已经给出了给出了明确的定义:

1
aastore value, index, arrayref

在运行时,value 的类型必须与 arrayref 引用的数组组件的类型兼容。具体来说,仅在以下情况下才允许将引用类型 S(源)的值赋给引用类型 T(目标)的数组组件:

  1. 如果 SClass 类型,则:
    • 如果 TClass 类型,则 S 必须与 T 属于同一类,或者 S 必须是 `T 的子类;
    • 如果 T 是接口类型,则 S 必须实现接口 T
  2. 如果 S 是接口类型,则:
    • 如果 TClass 类型,则 T 必须是 Object
    • 如果 T 是接口类型,则 T 必须是与 S 相同的接口或 S 的超接口。
  3. 如果 S 是数组类型 SC[],即数组组件为 SC 类型,则:
    • 如果 TClass 类型,则 T 必须是 Object
    • 如果 T 是接口类型,则 T 必须是由数组实现的接口之一(JLS§4.10.3)。
    • 如果 T 为数组类型 TC[],即数组组件为 TC 类型,则以下条件之一必须为 true
      • TCSC 是相同的原始类型。
      • TCSC 是引用类型,类型 SC 可赋给 TC

所以,根据上面的最后一条规则,String[] 是可以赋给 Object[],因为:

  1. String[]Object[] 都为引用类型
  2. String 可以赋给 Object

既然这样,那么,下面这段代码的结果是怎样的呢?

1
2
3
4
5
6
7
public static void main(String[] args) {
String[] a = new String[2];
Object[] b = a;
System.out.println(String[].class.getSuperclass());
System.out.println(Object[].class.getSuperclass());
System.out.println(Object[].class.isAssignableFrom(String[].class));
}

运行一下,结果为:

1
2
3
class java.lang.Object
class java.lang.Object
true

这说明了什么问题:

  1. 数组不存在继承关系
  2. 不存在继承关系的两种不同的引用类型之间可以赋值(这难道不是与多态的定义相违悖?)

Kotlin 的表现

如果把上面的代码翻译成 Kotlin,会不会有不一样的结果呢?

看来 Kotlin 的类型系统相对于 Java 来说更严谨。