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()会在下次扩容的大小和当前元素数量之间选择一个较大值
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现