ArrayList的扩容机制

前置知识

ArrayList的底层实现是一个Object[],而LinkedList的底层实现是一个链表
ArrayList与LinkedList相对比:

  • ArrayList在随机访问时可以做到O(1),但是LinkedList的随机访问就是遍历链表,所以时间复杂度是O(N)
  • ArrayList在插入/删除元素时,需要移动额外的很多元素,但是LinkedList在插入/删除时无需移动其他元素,效率更高
  • 如果数据需要频繁的插入/删除,那么建议使用LinkedList
  • 如果数据较为稳定,希望高效的访问元素,建议使用ArrayList
利用反射获取ArrayList容量

由于ArrayList中并没有提供获取底层数组的方法,如何利用反射来获取ArrayList的真实容量

public static int getCapatity(ArrayList list) {
int len = 0;
try {
// 获取Class对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 获取类属性
Field field = clazz.getDeclaredField("elementData");
// 暴力打开权限
field.setAccessible(true);
Object[] objList = (Object[]) field.get(list);
// 返回数组长度
len = objList.length;
} catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
// 返回数组长度
return len;
}

ArrayList的扩容机制

首先,创建ArrayList的方式有哪些
ArrayList提供了三个构造方法来创建ArrayList

// 1. 无参构造
public ArrayList()
// 2. 指定容量大小
public ArrayList(int initialCapacity)
// 3. 指定元素来创建
public ArrayList(Collection<? extends E> c)
无参构造

当调用无参构造来创建ArrayList时,看此无参构造方法的源码

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

其中elementData就是ArrayList实现的Object[ ],在无参构造中,直接为数据赋了一个默认的空元素值,也就是此时的容量为0
当ArrayList的容量不足时,扩容的机制是:
当目前的容量是0时,第一次扩容后的容量为10,以后每次扩容后的容量都是原始容量的1.5倍!!!
但是此处的1.5倍并不是在算术意义上的size*1.5,因为计算机并不能真正处理浮点数
此处是1.5倍是位运算的右移1位,能够避免浮点数的问题
即:

扩容后的容量 = 原始容量 + 原始容量 >> 1

当容量不足时,会创建一个新的指定大小的新数组,将原始数组中的内容拷贝到新的数组中(旧的数组不被再引用,就会被垃圾回收掉)
分析以下代码来试试:

// 1. 通过无参构造创建,此时容量为0
ArrayList list = new ArrayList();
// 2. 向容器中添加元素
list.add(5);
// 此时容量不足,触发第一次扩容,此时容量为10
// 3. 继续添加10个元素,添加后共有11个元素
for(int i = 1; i <= 10 ;i ++){
list.add(i);
}
// 当要插入第11个元素时,检查发现此时容量不足,触发第二次扩容
// 扩容后的容量为 = 10 + 10 >> 1 = 15;
// 4. 此时有11个元素,容量为15
// 继续添加
for(int i = 1;i <= 10 ; i ++){
list.add(i);
}
// 当添加第16个元素时,发现容量又不足了,所以此时触发第三次扩容
// 新的容量 = 15 + 15 >> 1 = 22
有参构造指定容量大小

来看利用ArrayList的指定容量大小的源码:如果指定的容量大于0,就会创建指定容量大小的数组。如果指定的容量小于0,抛出异常。

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

当指定了ArrayList的初始容量后,以后每次的扩容就是 扩容后的容量 = 原始容量 + 原始容量 >> 1,如果初始容量为0,还是会第一次扩容为10

指定元素创建

直接传入一个集合,来创建包含指定元素的ArrayList

ArrayList list = new ArrayList(Arrays.asList(1,2,3));

此时会使用指定的元素大小作为起始容量,以后的扩容规则还是同上。

addAll()

add()方法是一次添加一个元素,当使用addAll()方法可以一次添加多个元素。
以上的扩容机制都是在add()方法触发的情况下,新的容量 = 原始容量 + 原始容量 >> 1
当addAll()触发扩容时,扩容机制是不一样的。
会在 默认下次扩容后的容量大小当前所有的元素数量 之间选择一个较大值

ArrayList list = new ArrayList();
// 此时容量为0
list.add(Arrays.asList(1,2,3));
// 此时容量不足,触发扩容,
// 此时的元素数量是3,默认下次扩容后的容量是10
// 选择10 作为扩容后的容量

再来看一个例子

Array list = new ArrayList();
// 刚创建,此时容量为0
// 继续添加,容量不足,触发扩容
list.addAll(Arrays.asList(1,2,3,4,5,6,7,8,9,10,11));
// 默认下次扩容后的容量是10, 但是此时的元素数量是11,
// 所以扩容后的容量为11

再来看一个例子

ArrayList list = new ArrayList();
for(int i = 1;i <= 10 ;i ++){
list.add(i);
}
// 此时容量为10,容量不足
list.addAll(Arrays.asList(1,2,3))
// 下次扩容后的容量为15,元素数量是13,
// 所以扩容后的容量是15

总结

三种创建ArrayList的方式:

  • ArrayList() 会创建一个起始容量为0的大小
  • ArrayList(int capacity)以指定的大小作为起始容量
  • ArrayList(Collection c)以指定的元素的数量作为起始容量

两种扩容机制:

  • add()如果原始容量是0,则扩容后的容量为10,以后每次扩容都是之前的1.5倍
  • addAll()会在下次扩容的大小和当前元素数量之间选择一个较大值
posted @   秋天Code  阅读(11)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示