在Java的类型系统中,数组有什么缺陷吗?

关于程序员对 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类型系统的“漏洞”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值