关于程序员对 Java 类型系统的理解,比较高级的一个面试问题是这样:
王垠原版的代码
public static void f() { String[] a = new String[2]; Object[] b = a; a[0] = "hi"; b[1] = Integer.valueOf(42); }
这段代码里面到底哪一行错了?为什么?如果某个 Java 版本能顺利运行这段代码,那么如何让这个错误暴露得更致命一些?
注意这里所谓的「错了」是本质上,原理上的。
那么这儿的“错误”是指什么呢?
TL;DR
如果只能用一句话回答这个问题的话,那么就是:
Java数组不支持泛型,破坏了Java的类型安全性
类型系统的一些前提
一个好的类型系统,能够尽可能早的检测出错误,比如你将一个String赋值给int变量的时候,编译器就会报错,而不是等程序跑起来再报错。
Java的数组设计坏在哪儿
为了表述简单,我们假设Java支持了范型数组哈,比如 <?>[]
这样的表示法。
王垠原版的代码
public static void f() { String[] a = new String[2]; // 1 Object[] b = a; // 2 a[0] = "hi"; // 3 b[1] = Integer.valueOf(42); // 4 }
上面这段代码,在第二步的时候,其实就出现了一丝不对劲。将一个 String[]
转化成一个 Object[]
,这导致了数组的类型细节“逃逸”除了类型系统。
或者用更加明白的话来说:在第四步,给一个 String[]
里面塞一个Integer对象的时候,编译器就应该报错。
如果能够重来,应该怎么设计
如果按照完美的类型系统来设计,王垠的代码应该是这个样子的:
用范型数组的方式来重写王垠的示例
public static void f() { // 1. a是一个数组,里面存储的是String或者子类 <String>[]a = new String[2]; // 2. b是一个数组,里面存储的类型是String的一个父类,比如是Object吧 <? super String>[]b = a; // 3. 往a里面写String a[0] = "hi"; // 4. 往b里面写一个Integer b[1] = Integer.valueOf(42); }
这个程序这回看起来正常了很多,而且根据Java范型的规则,在第二步也能顺利触发编译失败。 <String>[]
转换成 <? super String>[]
,这当然不能成功,要不然后面把Integer对象往里塞的时候,类型系统就没法判断了。
问题还没有结束
把 <String>[]
转换成 <? super String>[]
,本质是为了读取:可以把String当作Object来读取。
难道范型数组,就没法支持这点了吗?
当然不。范型的上下界就是用来做这些限定的,示例代码如下:
用范型数组的方式来重写王垠的示例
public static void f() { // a为一个数组,里面存储的是String或者子类 <String>[] a = new String[100]; // 写入 // b中存储的是String的一个父类(也有可能是String,下同),String是下界 <? super String>[] writeonlyA = a; // 这时候就可以写入元素了(符合下界限定) writeonlyA[0] = "Hi"; // 无法读取元素(无法符合上界限定) // 编译器报错,无法推断elem的类型 // elem = writeonlyA[0]; // 读取 // d为一个数组,里面存储的类型是String的一个子类,编译器会把它当作String来处理 <? extends String>[] readonlyA = a; // 从readonlyA读取 String elem = readonlyA[0]; // 向readonlyA写入 // 编译器报错,无论等号右面是什么类型,都无法保证符合类型约定,因为readonlyA没有明确的下界 // readonlyA[0] = "Hi"; }
<? super T> <? extens T>
有没有更简单的表述呢?
在类型系统中,List和array是类似的,正好Java的List支持了范型,那么我们用List重写上面的例子:
用List来重写王垠的示例
public static void f() { // a为一个数组,里面存储的是String或者子类 List<String> a = new ArrayList<String>(); // **类型安全的写入** // b中存储的是String的一个父类 List<? super String> writeonlyA = a; // 这时候就可以写入元素了(符合上界限定) writeonlyA.add("Hi"); // 从writeonlyA里面读取的类型只能是Object // 因为我们将a转为了更加“宽泛”的类型了 Object x = writeonlyA.get(1); // 如果你想写入Integer(王垠的例子) // 下面这一句会报错 // List<? super Integer> c = a; // **类型安全的读取** // d为一个数组,里面存储的类型是String的一个子类 List<? extends String> readonlyA = a; // 往a里面写String a.add("hi"); // 从readonlyA里面读取,类型系统可以很好的约束这个行为 String xx = readonlyA.get(0); // 尝试写入的话,没有明确下界,无法写入,编译器会报错 // readonlyA.add("d"); }
可以看到,上面用List的程序,用类型系统+范型的上下界,很完美的限制了类型不安全的操作。
但是,由于数组array不支持范型,导致JVM在实现的时候,只能将数组处理成协变的,允许了类型不安全的转换操作,导致了Java类型系统的“漏洞”。