首页 > 其他分享 >一个 Bug JDK 居然改了十年?

一个 Bug JDK 居然改了十年?

时间:2024-12-25 20:19:11浏览次数:2  
标签:toArray JDK Arrays ArrayList Object 改了 数组 Bug size

问题现象

今天偶然看到了一个 JDK 的 Bug,给大家分享一下。

假设现在有如下的代码:

List<String> list = new ArrayList<>();
list.add("1");
Object[] array = list.toArray();
array[0] = 1;
System.out.println(Arrays.toString(array));

上面的代码是可以正常支执行的,如下图所示:
image.png

修改代码为如下代码:

List<String> list = Arrays.asList("1");
Object[] array = list.toArray();
array[0] = 1;
System.out.println(Arrays.toString(array));

再次执行代码,结果就会抛出 ArrayStoreException 异常,这个异常表明这里并不能把一个 Integer 类型的对象存放到这个数组里面。如下图所示:

查看 Arrays 的静态内部类 ArrayListtoArray() 方法的返回值就是 Object[] 类型的,如下图所示:

image.png

这里就会引发一个疑问: 为啥使用 java.lang.util.ArrayList 代码就可以正常运行?但是使用 Arrays 的静态内部类 ArrayList 就会报错了?

原因分析

首先看下 java.lang.util.ArrayList 类的 toArray() 方法的实现逻辑:

image.png
从上面可以看出 toArray() 方法是拷贝了一个 ArrayList 内部的数组对象,然后返回的。而 elementData 这个数组在实际初始化的时候,就是 new 了 Object 类型的数组。如下图所示:

image.png

那么经过拷贝之后返回的还是一个实际类型为Object 类型的数组。既然这里是一个 Object 类型的数组,那么往里面放一个 Integer 类型的数据是合法的,因为 ObjectInteger 类型的父类。

然后再看下 Arrays 的静态内部类 ArrayListtoArray() 方法的实现逻辑。这里返回的是 a 这个数组的一个克隆。如下图所示:

image.png

而这个 a 数组声明的类型是 E[],根据泛型擦除后的原则,这里实际上声明的类型也变成了 Object[]。 如下图所示:

image.png

那接下来再看看 a 实际的类型是什么? 由于 Arrays 的静态内部类 ArrayList 的构造函数是包级访问的,因此只能通过 Arrays.asList() 静态方法来构造一个这个对象。如下图所示:

image.png

Arrays.asList() 方法的签名是变长参数类型,这个是 Java 的一个语法糖,实际对应的是一个数组,泛型擦除后就变成了 Object[] 类型。如下图所示:

image.png

而在代码实际调用处,实际上会 new 一个 String 类型的数组,也就是说 「a 的实际类型是一个 String 类型的数组」。 那么 a 调用了 clone() 方法之后返回的类型也是一个 String 类型的数组,克隆嘛,类型一样才叫克隆。如下图所示:

image.png

经过上面的分析,答案就呼之欲出了。a 的实际类型是一个 String 类型的数组,那么往这个数组里面放一个 Integer 类型的对象那肯定是要报错的。等效代码如下图所示:

image.png

为什么是个Bug ?

查看 Collection 接口的方法签名,方法声明明确是要返回的是一个 Object[] 类型的数组,因为方法明确声明了返回的是一个 Object[] 类型的数组,但是实际上在获取到了这个返回值后把它当作一个 Object[] 类型的数组使用某些情况下是不满足语义的。

同时这里要注意一下,返回的这个数组要是一个 「安全」的数组,安全的意思就是「集合本身不能持有对返回的数组的引用」,即使集合的内部是用数组实现的,也不能直接把这个内部的数组直接返回。这就是为什么上面两个 toArray() 方法的实现要么是把原有的数组复制了一份,要么是克隆了一份,本质上都是新建了一个数组。如下图所示:

image.png

在 OpenJDK 的 BugList 官网上很早就有人提出这个问题了,从时间上看至少在 2005 年就已经发现这个 Bug 了,这个 Bug 真正被解决是在 2015 年的时候,整整隔了 10 年时间。花了 10 年时间修这个 Bug,真是十年磨一剑啊!
image.png
image.png

如何修正的这个 Bug ?

JDK 9 中的实现修改为了新建一个 Object 类型的数组,然后把原有数组中的元素拷贝到这个数组里面,然后返回这个 Object 类型的数组,这样的话就和 java.util.ArrayList 类中的实现方法一样了。
image.png

java.util.ArrayList 类的入参为 Collection\<? exends E> 类型的构造函数中就涉及到可能调用 Arrays 的静态内部类 ArrayListtoArray() 方法,JDK 在实现的时候针对这个 Bug 还做了特殊的处理,不同厂商发行的 JDK 处理方式还有细微的不同。

Oracel JDK 8 版本的实现方式
image.png

Eclipse Temurin Open JDK 8 版本的实现方式
image.png

之所以在 java.util.ArrayList 对这个 Bug 做特殊的处理是因为 Sun 公司在当时选择不修复改这个Bug,因为怕修复了之后已有的代码就不能运行了。如下图所示:

image.png
image.png

比如在修复前有如下的代码,这个代码在 JDK 8 版本是可以正常运行的,如下图所示:

String[] strings = (String[]) Arrays.asList("foo", "bar").toArray();  
for (String string : strings) {  
    System.out.println(string);  
}

但是如果升级到 JDK 9 版本,就会报 ClassCastException 异常了,如下图所示:

image.png

因为修复了这个 Bug 之后,编译器并不能告诉你原来的代码存在问题,甚至连新的警告都没有。假设你从 JDK 8 升级到 JDK 9 了,代码也没有改,但是突然功能就用不了,这个时候你想不想骂人,哈哈哈哈。这也许就是 Sun 公司当年不愿意修复这个 Bug 的原因之一了。当然,如果你要问我为什么要升级的话,我会说:你发任你发,我用 Java 8 !

题外话

阿里巴巴的 Java开发手册toArray(T[] array) 方法的调用有如下的建议:

image.png
这里以 java.util.ArrayList 类的源码作为参考,源码实现如下:

// ArrayList 的 toArray() 方法实现:
public <T> T[] toArray(T[] a) {  
    if (a.length < size)  // 如果传入的数组的长度小于 size 
        // Make a new array of a's runtime type, but my contents:  
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());  
    System.arraycopy(elementData, 0, a, 0, size);  
    if (a.length > size)  
        a[size] = null;  
    return a;  
}
// Arrays 的 coypyOf 方法实现:
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {  
    @SuppressWarnings("unchecked")  
    T[] copy = ((Object)newType == (Object)Object[].class)  
        ? (T[]) new Object[newLength]  
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);  
    System.arraycopy(original, 0, copy, 0,  
                     Math.min(original.length, newLength));  
    return copy;  
}

当调用 toArray() 方法时传入的数组长度为 0 时,方法内部会根据传入的数组类型动态创建一个和当前集合 size 相同的数组,然后把集合的元素复制到这个数组里面,然后返回。

当调用 toArray() 方法时传入的数组长度大于 0,小于 ArrayList 的 size 时,走的逻辑和上面是一样的,也会进入到 ArayscopyOf 方法的调用中,但是调用方法传入的新建的数组相当于新建之后没有被使用,白白浪费了,需要等待 GC 回收。

当调用 toArray() 方法时传入的数组长度大于等于 ArrayList 的 size 时,则会直接把集合的元素拷贝到这个数组中。如果是大于的情况,还会把数组中下标为 size 的元素设置为 null,但是 size 下标后面的元素保持不变。如下所示:

List<String> list = new ArrayList<>();  
list.add("1");  
String[] array = new String[3];  
array[1] = "2";  
array[2] = "3";  
String[] toArray = list.toArray(array);  
System.out.println(array == toArray);  
System.out.println(Arrays.toString(toArray));

image.png

手册中提到的在高并发的情况下,传入的数组长度等于 ArrayList 的 size 时,如果 ArrayList 的 size 在数组创建完成后变大了,还是会走到重新新建数组的逻辑里面,仍然会导致调用方法传入的新建的数组没有被使用,而且这里因为调用方法时新建的数组和 ArrayList 之前的 size 相同,会造成比传入长度为 0 的数组浪费多得多的空间。但是我个人觉得,因为 ArrayList 不是线程安全的,如果存在数据竞争的情况就不应该使用。

参考

Arrays.asList(x).toArray().getClass() should be Object[].class
array cast Java 8 vs Java 9
toArray方法的小陷阱,写开发手册的大佬也未能幸免
.toArray(new MyClass[0]) or .toArray(new MyClass[myList.size()])?
Arrays of Wisdom of the Ancients
Java开发手册(黄山版).pdf

标签:toArray,JDK,Arrays,ArrayList,Object,改了,数组,Bug,size
From: https://www.cnblogs.com/javadaydayup/p/18631346

相关文章

  • JDK-8中的JAVA_OPTS通常用于传递给JVM的启动参数
    在JDK8中,JAVA_OPTS通常用于传递给JVM的启动参数。以下是一些常见的JAVA_OPTS项及其说明:内存管理-Xms:设置Java堆的初始大小,例如-Xms512m。-Xmx:设置Java堆的最大大小,例如-Xmx1024m。-Xmn:设置年轻代的大小。-XX:PermSize=size:设置永久代的初始大小(在JDK8中被Metaspace取代......
  • 【原创】【踩坑日记】onnx转ncnn模型出bug
    python导出的onnx格式的yolo模型为yolov11.onnx文件。输出代码如下:fromultralyticsimportYOLOmodel=YOLO("yolo11n.pt")success=model.export(format="onnx")ncnn需要两个模型文件才能推理,.param和.bin文件。官方建议使用onnx2ncnn.exe导出,这个导出的onnx格式文件(yo......
  • Bugzilla 安装及使用指南:助力软件缺陷精准管控
    Bugzilla是一个开源的缺陷跟踪系统,在软件开发和测试过程中被广泛使用。一、主要功能缺陷提交开发团队成员、测试人员或者用户可以通过Bugzilla提交软件中发现的问题。提交时需要详细描述缺陷的症状,比如软件在执行某个特定操作时出现的错误提示信息、软件崩溃的具体场景等。例......
  • 【保姆级教程】10分钟搞定Linux安装JDK,小白也能轻松上手!
    本文将详细介绍如何在Linux系统上安装配置JDK环境,包括下载、安装、环境变量配置等全过程。一、准备工作1.1检查系统是否已安装JDKjava-version如果显示"commandnotfound",说明系统中还没有安装JDK。1.2下载JDK安装包访问Oracle官网下载页面:https://www.oracle.com/j......
  • 「Bugku CTF」7+1+0
    题意7+1+0?格式bugku{xxxxx}。4nXna/V7t2LpdLI44mn0fQ==分析看到末尾有==,第一反应就是base64,但使用BugkuCTF自带的base64解密工具发现ServerError,猜想到这串base64码被动过手脚。这里有一个小插曲,我使用「鲁棒性较强的base64解密网站」,发现偶数位得到了正确的......
  • 反向 Debug 了解一下?揭秘 Java DEBUG 的基本原理
    作者:京东保险蒋信Debug的时候,都遇到过手速太快,直接跳过了自己想调试的方法、代码的时候吧……一旦跳过,可能就得重新执行一遍,准备数据、重新启动可能几分钟就过去了。好在IDE们都很强大,还给你后悔的机会,可以直接删除某个StackFrame,直接返回到之前的状态,确切的说是返回到之......
  • 「Bugku CTF」Easy Math
    题意题目给出了一份task.py,内容如下:fromCrypto.Util.numberimportgetPrime,bytes_to_longfromsecretimportflagp=getPrime(2048)q=getPrime(2048)x=getPrime(4096)y=getPrime(4096)n=p*qe=0x10001c=pow(bytes_to_long(flag),e,n)print(c)......
  • Java 大视界 -- Java 构建大数据开发环境:从 JDK 配置到大数据框架集成(一)
           ......
  • 禅道bug增加自定义字段
    禅道版本18.9需求给禅道的bug模块,增加自定义字段。目前主要增加“发现阶段”、“所属环境”、“出现频率”增加bug的类型在bug列表增加搜索:“发现阶段”、“所属环境”、“出现频率”在测试报告增加模块:“发现阶段”、“所属环境”、“出现频率”禅道开发手册https://w......
  • 最新版的IDEA运行java时怎么看运行过程此操作为(IDEA Debug)
    想必每个学习java的帅哥美女们在运行循环代码的时候,直接看到结果是感觉不真实的,小编今天在学习数组时研究了一个小时都怪我没有好好牢记Random的使用导致了代码的疑问性为此IDEA中有个好用的操作可以查看运行时的步骤是怎样一步一步跑的,废话不多说看下图如图所示为运行步骤......