一个 Bug JDK 居然改了十年?

news/2024/12/25 20:18:31/文章来源:https://www.cnblogs.com/javadaydayup/p/18631346

问题现象

今天偶然看到了一个 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/858809.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

崩溃列表数据库查询(可供参考)

首先去https://weikezhijia.feishu.cn/sheets/BIvxsKZhHhzpC6tDyoLcPE50n4d?sheet=9ikXjx 看库中导出指标,然后可以查到是rum_error表,然后去ors_rum_test服务器,ors_rum_test数据库,rum_error表去查询,结合点击全部按钮后查看F12 然后查看preView里的字段,结合着去查rum…

流量分析练习

flag明文 题目:key.pcapng查找flag或者flag{,我们在下图查找到所需要的flag本类题目特点:能够在字节流中直接查找到带有flag的字符串,不存在加解密或转换等,属于明文形式 2.flag编码 题目:64da5a4a1e024d198dfa307299965b6d.pcapng本题考到十六进制编码 将flag转成十六进…

哪里有 class 告诉我?

说明 本文中的 JVM 参数和代码在 JDK 8 版本生效。 哪里有用户类? 用户类是由开发者和第三方定义的类,它是由应用程序类加载器加载的。 Java 程序可以通过CLASSPATH 环境变量,JVM 启动参数 -cp 或者 -classpath 指定用户需要加载的类的路径。这两个配置的优先级从低到高,后…

python多进程之间通讯,消息队列Queue

代码:from multiprocessing import Process, Queuedef producer(q):myinfo = "包子"q.put(myinfo)print(f"生产了{myinfo}")myinfo = "饺子"q.put(myinfo)print(f"生产了{myinfo}\n") 生产了4个,消费5个def consumer(q):print(f&q…

使用DBeaver连接带有Kerberos认证的hive(亲测可用)

先下载工具 https://yvioo.lanzn.com/isBg42j0fu7e里面是两个文件 一个jar包 一个安装包 首先点击kfw-4.1-amd64.msi 进行安装,建议直接默认配置安装 选择"TYPE" 安装完成后 点击 1、先配置环境变量 第一个变量名:KRB5_CONFIG 变量值: 这个就是Kerberos认证给的k…

【QTTabBar】批量去除当前文件夹的所有文件只读属性

使用方法参考: https://www.cnblogs.com/issacnew/p/18392262// 作者:博客园-issacnew // 网站:https://www.cnblogs.com/issacnew/p/18392262 // 作用:qttabbar去除当前文件夹下的所有文件只读属性,使得所有文件可读var qs = new ActiveXObject("QTTabBarLib.Script…

C#使用Python.NET执行Python脚本文件踩坑总结

在VS,Nuget包管理器搜索“Python.NET”,安装pythonnet包,如下图: C#使用Python.NET执行Python脚本文件,C#代码如下:1 public class PythonExecuter2 {3 private readonly string _pythonDllPath;4 private readonly string _workDir;5 6 public PythonExecut…

PoerPC平台下的ethtool工具的编译

1. 编译器安装 2. 编译过程 参考:ethtool工具源码交叉编译_ethtool交叉编译-CSDN博客 注:步骤3中的config配置中,通过sudo apt-get install pkg-config libmnl-dev修复完问题后,需要重新执行一次步骤2中的./autogen.sh下载目标程序

微信小程序开发总结

业务需要,最近又搞起了微信小程序,之前从来没有参与过小程序的开发,对于开发中的流程也是知之甚少,正好学习一下,开搞... 前提:使用企业注册小程序 微信认证 小程序备案 [本地开发] 1.获取appid和secret 管理 > 开发管理 获取即可, 需要管理员扫码确认这里获取到的appid在使…

windows 下面使用 celery 管理定时任务

Python 实现定时任务有以下几种思路使用子进程(现成)+ time.sleep 间隔执行 使用现有的库管理定时任务如,celery, tornado等 使用系统的机制执行linux 下面 crontab ,windows 下面taskschd.msc本次调查 celery 这个常用的异步任务管理框架,它有一下好处支持分布式 支持任…

Goby 漏洞发布|CVE-2024-9047 WordPress File Upload 插件 wfu_file_downloader.php 任意文件读取漏洞

漏洞名称:CVE-2024-9047 WordPress File Upload 插件 wfu_file_downloader.php 任意文件读取漏洞 English Name:CVE-2024-9047 WordPress File Upload Plugin wfu_file_downloader.php Arbitrary File Read Vulnerabilit CVSS core: 6.8 漏洞描述: WordPress File Upload插件…

002. 队列安排(洛谷P1160)

002. 队列安排(洛谷P1160) 题目描述 一个学校里老师要将班上 \(N\) 个同学排成一列,同学被编号为 \(1\sim N\),他采取如下的方法:先将 \(1\) 号同学安排进队列,这时队列中只有他一个人;\(2\sim N\) 号同学依次入列,编号为 \(i\) 的同学入列方式为:老师指定编号为 \(i\) …