0%

解决JDK 17中对java api的反射限制

最近在看Java的源码,想测试下ArrayList的动态长度扩增机制,得用反射获取其中数组elementData的长度。

代码如下:

1
2
3
4
5
6
7
8
List<Integer> list = new ArrayList<>();
Field field = list.getClass().getDeclaredField("elementData");
field.setAccessible(true);
for (int i = 0; i < 100; i++) {
Object[] elementData = (Object[]) field.get(list);
System.out.println(i + "," + "list size:" + list.size() + ", element array length:" + elementData.length);
list.add(i);
}

而调用的时候会抛出以下的错误:

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进行封装的两个理由:

  1. 对java代码进行反射是不安全的,比如可以调用ClassLoader的defineClass方法,这样在运行时候可以给程序注入任意代码。
  2. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0,list size:0, element array length:0
1,list size:1, element array length:10
...
9,list size:9, element array length:10
10,list size:10, element array length:10
11,list size:11, element array length:15
...
15,list size:15, element array length:15
16,list size:16, element array length:22
...
22,list size:22, element array length:22
23,list size:23, element array length:33
...
32,list size:32, element array length:33
33,list size:33, element array length:33
34,list size:34, element array length:49
...
48,list size:48, element array length:49
49,list size:49, element array length:49
50,list size:50, element array length:73
...
72,list size:72, element array length:73
73,list size:73, element array length:73
74,list size:74, element array length:109
...
98,list size:98, element array length:109
99,list size:99, element array length:109

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),异常恐怖。