默认构造函数创建的ArrayList未分配实际数组,elementData指向共享空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA;首次add()才延迟初始化为长度10的数组。

默认构造函数创建的 ArrayList 实际上没分配数组
调用 new ArrayList() 后,elementData 字段不是 null,而是指向一个共享的空对象数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。这个数组长度为 0,但类型是 Object[],它不等于 {}(new Object[0]),而是 JVM 内部的一个静态常量引用。此时任何 size()、isEmpty() 都能正常工作,但底层没真正分配可存元素的数组空间。
第一次 add() 触发扩容逻辑,才真正初始化数组
当你调用 add() 方法时,ArrayList 检查到当前 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就会走“首次扩容”分支,直接分配长度为 10 的数组 —— 这就是延迟初始化的核心行为。注意:不是从 0 扩到 1,而是跳到默认容量 10。
-
add(null)、add("a")都会触发该逻辑 - 如果在
add()前调用ensureCapacity(1),也会提前分配数组(但不会用 10,而是按传入值) - 反射读取
elementData字段可验证:构造后是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,add()后变成长度为 10 的Object[]
为什么不用 new Object[0] 作为初始值?
因为需要区分“用户显式指定容量为 0”和“使用默认构造函数”。前者用的是 EMPTY_ELEMENTDATA,后者用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。这样在 add() 时才能判断是否走“首次扩容到 10”,而不是错误地扩容成 1。
-
new ArrayList(0)→elementData = EMPTY_ELEMENTDATA→add()后扩容为 1(非 10) -
new ArrayList()→elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA→add()后扩容为 10 - 这个区分只在第一次
add()时起作用;后续扩容都按 1.5 倍增长
调试时怎么确认数组是否已初始化?
最直接的方式是用反射获取 elementData 并检查其长度和引用身份:
ArrayList<String> list = new ArrayList<>();
Field f = ArrayList.class.getDeclaredField("elementData");
f.setAccessible(true);
System.out.println(f.get(list)); // 输出类似 [Ljava.lang.Object;@xxx(未初始化时其实是同一静态引用)
list.add("x");
System.out.println(f.get(list)); // 输出类似 [Ljava.lang.Object;@yyy,且 .length == 10
注意 JDK 版本差异:DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在 JDK 7+ 引入,JDK 6 中默认构造函数直接分配了长度为 10 的数组,没有延迟初始化。
延迟初始化的代价很小,但容易被误认为“构造就分配”,实际只有第一次写操作才真正扛起内存分配责任 —— 这个边界点,恰恰是很多内存分析或序列化场景里被忽略的关键。

















