最近在看Java的源码,想测试下ArrayList的动态长度扩增机制,得用反射获取其中数组elementData的长度。
代码如下:
1 | List<Integer> list = new ArrayList<>(); |
而调用的时候会抛出以下的错误:
Exception in thread “main” java.lang.reflect.InaccessibleObjectException: Unable to make field transient java.lang.Object[] java.util.ArrayList.elementData accessible: module java.base does not “opens java.util” to unnamed module @7a81197d
JDK 17对反射的限制
根据Oracle的文档,为了安全性,从JDK 17开始对java本身代码使用强封装,原文叫Strong Encapsulation。任何对java.*代码中的非public变量和方法进行反射会抛出InaccessibleObjectException异常。
JDK的文档解释了对java api进行封装的两个理由:
- 对java代码进行反射是不安全的,比如可以调用ClassLoader的defineClass方法,这样在运行时候可以给程序注入任意代码。
- java的这些非公开的api本身就是非标准的,让开发者依赖使用这个api会给JDK的维护带来负担。
所以从JDK 9开始就准备限制对java api的反射进行限制,直到JDK 17才正式禁用。
解决方法
根据文档,使用–add-opens的命令行参数可以解除对指定java api的反射限制。
比如要解决调用上面代码的异常,ArrayList在java.util包下,要加入以下的命令行参数:
1 | --add-opens java.base/java.util=ALL-UNNAMED |
然后会打印以下内容:
1 | 0,list size:0, element array length:0 |
ArrayList动态长度扩增机制
ArrayList一开始数组长度设置为0,增加一个element会将长度直接扩增到10。后面如果数组满了会把数组长度扩增现有长度的一半。
对于n个element的list,插入的平均时间复杂度为o(n),因为数组的扩增并不会很频繁。
这里简单证明下,a1为初始的数组长度,比如10:
a1 * (1.5 ^ x) ~= n
x次拷贝的长度加一起为:
a1 * 1.5 + a1 * (1.5 ^ 2) + … + a1 * (1.5 ^ x) = 3 * a1 * (1.5 ^ x - 1) ~= 3n
ArrayList的remove操作
这里没有放测试代码,有兴趣可以自己写下。
如果对ArrayList进行remove操作,ArrayList不会缩小elementData的长度,而是直接把删除元素后面的所有元素移动到删除的位置。这个可以理解,毕竟对list删除某个元素的场景不多,如果真的要反复添加删除不如用Deque。
所以删除末尾元素的时间复杂度是o(1),删除头元素的时间复杂度为o(n)。如果不断的删除头元素直到list为空,则时间复杂度会是o(n ^ 2),异常恐怖。