解读王垠博客“一道 Java 面试题”
偶然拜读IT界知名大佬王垠老师的博客,发现一个有意思的题目:
1 // 这段代码里面到底哪一行错了?为什么? 2 // 原文:http://www.yinwang.org/blog-cn/2020/02/13/java-type-system 3 public static void f() { 4 String[] a = new String[2]; 5 Object[] b = a; 6 a[0] = "hi"; 7 b[1] = Integer.valueOf(42); 8 }
虽然小菜才疏学浅,但本着学习交流的态度,写下此篇文章来分析一下这个问题。
首先我们要读懂每一行代码在做什么:
String[] a = new String[2]; 定义一个字符串类型的数组a,并初始化。
Object[] b = a; 定义一个对象类型的数组b,并将字符串类型数组a赋值给b。
a[0] = "hi"; 使用变量a访问数组中的第一个元素,赋值。
b[1] = Integer.valueOf(42); 使用变量b访问数组中的第二个元素,赋值。
只有简单的四行代码,相信读者都可以看的懂。
先不考虑太多,直接执行一下代码,编译通过,运行报错:java.lang.ArrayStoreException: java.lang.Integer。
错误提示我们第四行代码有问题,不可以将整型数据存储到数组b中,而b是一个Object类型的数组,编译通过,却无法赋值。
分析一下原因,数组b的引用指向数组a,我们操作数组b,实际在内存中,访问的应该是数组a,而数组a是一个字符串类型数组,整个过程中,并不存在Object类型的数组,仅有一个字符串类型的数组在内存中被创建,如图:
变量a和变量b只不过是门面,通过这两道门,到达的是同一个房间。只不过a门只允许String类型通过,而b门没有任何限制。
因此,假如我们写下a[0] = Integer.valueOf(42);,编译器立刻会发现错误,提示类型错误,而b[1] = Integer.valueOf(42);的写法是符合规则的,但由于实际数据结构是String数组,所以运行肯定无法通过。
为什么会这样?出现这种问题的根本原因,在于Object[] b = a;,严格来说,这种语法是错误的,但是在JDK规范中却被认可。
为什么说是错误的?面向对象中的继承我们再熟悉不过了,子类完全具有父类的能力,子类可以退化成为父类。
单说String的确是Object的子类,完全符合规则,但数组是另一回事,本例中String数组仅仅能容纳String类型的元素,而Object数组可以容纳任意类型的元素,String数组并非完全具有Object数组的能力。
从另一个角度看,无论是String[] a还是Object[] b,这两种写法中的变量a和变量b,仅仅能决定指针的指向(引用哪个具体的数组),而无法控制数组内的元素,只能整体操作,而数组必然要涉及某个元素的部分操作,这就造成数组内部数据结构的“逸出”,必然会出现问题。
综上,数组之间的抽象是错误的,数组之间没有直接的继承的能力,不属于面向对象继承的讨论范畴。
实际编写代码时,不必过分纠结这个问题,尽量不使用这种危险的操作,而是用更加优雅的方式去实现:
1 // 这样就能很好的发现错误,避免给自己挖坑 2 public static void f() { 3 String[] a = new String[2]; 4 Object b = a; //数组本身也是对象 5 a[0] = "hi"; 6 ((String[]) b)[1] = Integer.valueOf(42); 7 }