java - 为什么使用不同的 ArrayList 构造函数会导致内部数组的增长率不同?
问题描述
我似乎在实现中偶然发现了一些ArrayList
我无法理解的有趣的东西。这是一些代码,说明了我的意思:
public class Sandbox {
private static final VarHandle VAR_HANDLE_ARRAY_LIST;
static {
try {
Lookup lookupArrayList = MethodHandles.privateLookupIn(ArrayList.class, MethodHandles.lookup());
VAR_HANDLE_ARRAY_LIST = lookupArrayList.findVarHandle(ArrayList.class, "elementData", Object[].class);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
public static void main(String[] args) {
List<String> defaultConstructorList = new ArrayList<>();
defaultConstructorList.add("one");
Object[] elementData = (Object[]) VAR_HANDLE_ARRAY_LIST.get(defaultConstructorList);
System.out.println(elementData.length);
List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");
elementData = (Object[]) VAR_HANDLE_ARRAY_LIST.get(zeroConstructorList);
System.out.println(elementData.length);
}
}
这个想法是,如果你创建一个ArrayList
这样的:
List<String> defaultConstructorList = new ArrayList<>();
defaultConstructorList.add("one");
看看它会报告什么elementData
(所有元素都保存在哪里) 。因此,您添加了一个元素 - 您将获得 9 个未使用的额外插槽。Object[]
10
另一方面,如果您这样做:
List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");
您添加一个元素,保留的空间仅用于该元素,仅此而已。
在内部,这是通过两个字段实现的:
/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
当你创建一个ArrayList
via new ArrayList(0)
-EMPTY_ELEMENTDATA
时将被使用。
当你创建一个ArrayList
通过new Arraylist()
-DEFAULTCAPACITY_EMPTY_ELEMENTDATA
使用。
我内心最直观的部分——简单地尖叫“删除DEFAULTCAPACITY_EMPTY_ELEMENTDATA
”,让所有的案件都处理EMPTY_ELEMENTDATA
;当然是代码注释:
我们将此与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时要膨胀多少
确实有道理,但是为什么一个膨胀到10
(比我要求的要多得多)而另一个膨胀到1
(完全符合我的要求)。
即使您使用List<String> zeroConstructorList = new ArrayList<>(0)
并不断添加元素,最终您也会达到elementData
比请求更大的点:
List<String> zeroConstructorList = new ArrayList<>(0);
zeroConstructorList.add("one");
zeroConstructorList.add("two");
zeroConstructorList.add("three");
zeroConstructorList.add("four");
zeroConstructorList.add("five"); // elementData will report 6, though there are 5 elements only
但是它的增长速度小于默认构造函数的情况。
这让我想起了HashMap
实现,桶的数量几乎总是比你要求的多;但是这样做是因为需要“两个的力量”桶,但这里的情况并非如此。
所以问题是 - 有人可以向我解释这种差异吗?
解决方案
即使在实现不同的旧版本中,您也可以准确地获得您所要求的内容,以及指定的内容:
ArrayList()
构造一个初始容量为 10 的空列表。
ArrayList(int)
构造一个具有指定初始容量的空列表。
因此,ArrayList
使用默认构造函数构造 将给你一个ArrayList
初始容量为 10,因此只要列表大小为 10 或更小,就不需要调整大小操作。
相反,带有int
参数的构造函数将精确地使用指定的容量,受制于指定为的增长策略
除了添加一个元素具有恒定的摊销时间成本这一事实之外,没有指定增长策略的细节。
即使您指定初始容量为零,它也适用。
Java 8 添加了优化,将十元素数组的创建推迟到添加第一个元素。这专门解决了ArrayList
实例(使用默认容量创建)长时间甚至整个生命周期都为空的常见情况。此外,当第一个实际操作是 时addAll
,它可能会跳过第一个数组调整大小操作。这不会影响具有显式初始容量的列表,因为通常会仔细选择这些列表。
如本答案所述:
根据我们的性能分析团队的说法,大约 85% 的 ArrayList 实例是以默认大小创建的,因此这种优化对于绝大多数情况都是有效的。
其动机是精确优化这些场景,而不是触及指定的默认容量,这是在ArrayList
创建时定义的。(虽然JDK 1.4是第一个明确指定它的)
推荐阅读
- angular - 如何将 Ionic 框架与自定义 webpack 配置集成到现有的 Angular 应用程序中?
- laravel - 如果用户使用 Laravel 不活跃,则抛出错误消息
- batch-file - 如何使用批处理文件将变量作为剪贴板发送
- javascript - Firebase 实时数据库 - 如何构建初始负载和侦听器
- angular - 如何在角度7中过滤数组的数组
- jenkins - 从 Jenkins 执行 denodo 任务
- matplotlib - 线性回归图解释
- python - 如何根据用户输入的日期获取第二天的日期?
- python-3.x - 一张图比较四张图
- powershell - 在过滤路径中搜索特定文件夹名称