当前位置: 首页 > news >正文

深入Java集合框架:ArrayList源码解析(JDK 8)

前言

最近在系统地复习 Java 基础,发现自己对集合框架的掌握一直停留在面试八股阶段,背得出ArrayList基于数组、查询快增删慢,但从来没真正点开过它的源码看一看。

于是诞生了这一篇帖子,跟着JDK 8的源码一步步搞清楚它底层到底是怎么玩的。这篇帖子就是我的学习记录,既是给自己留个备忘,也希望能为同样在啃源码的小伙伴提供一些参考。如果有理解不到位的地方,欢迎大家在评论区指正!

一、ArrayList 继承体系与核心属性

1. 类继承关系

ArrayList位于java.util包下,它的核心继承与实现关系如下:

  • 继承AbstractList:提供了 List 接口的骨架实现。

  • 实现List接口:定义列表的操作规范。

  • 实现RandomAccess接口标记接口,表明支持快速随机访问(底层用 for 循环遍历比用迭代器快)。

  • 实现Cloneable接口:支持克隆(浅拷贝)。

  • 实现Serializable接口:支持序列化。

2. 核心成员变量

打开 ArrayList 的源码,我们会看到几个非常关键的属性:

// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;// 用于空实例的共享空数组(带初始容量0时)
private static final Object[] EMPTY_ELEMENTDATA = {};// 用于默认大小空实例的共享空数组(无参构造时)
// 和无参构造区分开,以便在第一次添加元素时知道要扩容多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 真正存放元素的数组缓冲区(ArrayList 的底层核心)
transient Object[] elementData;// 当前列表中实际存放的元素个数(注意:不是数组长度)
private int size;// 数组能分配的最大大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

二、构造方法详解

ArrayList 提供了三种构造方法:

1. 无参构造(最常用)

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

JDK 8 中,无参构造并没有立刻初始化一个长度为 10 的数组,而是先指向一个共享的空数组。真正的扩容发生在第一次添加元素时。

2. 指定初始容量

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);}
}

建议:如果事先知道要存大量数据,请使用此构造方法指定容量,避免频繁扩容带来的性能损耗!

3. 包含指定集合

public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray() 可能返回的不是 Object[] 类型,需要做防御性拷贝if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {this.elementData = EMPTY_ELEMENTDATA;}
}

三、动态扩容机制

扩容是 ArrayList 最核心的机制,发生在添加元素时。我们以 add(E e) 方法为入口:

1. add 方法入口

public boolean add(E e) {// 确保内部容量足够,这是扩容的核心判断ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}

2. 扩容流程追踪

private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}private static int calculateCapacity(Object[] elementData, int minCapacity) {// 如果是无参构造后的第一次添加,取默认容量 10 和 所需最小容量 的较大值if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}private void ensureExplicitCapacity(int minCapacity) {modCount++; // 修改次数+1,为 Fail-Fast 机制服务// 如果所需最小容量 > 当前数组长度,执行扩容if (minCapacity - elementData.length > 0)grow(minCapacity);
}

3. grow() 方法(真正的扩容逻辑)

private void grow(int minCapacity) {int oldCapacity = elementData.length;// 新容量 = 旧容量 + 旧容量的一半(位运算,相当于 1.5 倍扩容)int newCapacity = oldCapacity + (oldCapacity >> 1);// 如果新容量仍然小于所需最小容量,直接等于最小容量if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量超过了最大数组大小限制if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 拷贝原数组到新数组(这是一个耗时的 O(n) 操作!)elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容总结

  • 扩容倍数:旧容量的 1.5 倍oldCapacity >> 1)。

  • 触发时机:当 size + 1 > elementData.length时。

  • 性能损耗:扩容会触发 Arrays.copyOf 进行全量数据拷贝,因此在能预估数据量时,务必指定初始容量

四、核心方法源码剖析

1. 指定位置插入 add(int index, E element)

public void add(int index, E element) {rangeCheckForAdd(index); // 检查下标越界ensureCapacityInternal(size + 1); // 检查扩容// 将 index 及其之后的所有元素向右移动一位(O(n) 操作)System.arraycopy(elementData, index, elementData, index + 1, size - index);elementData[index] = element;size++;
}

结论:在 ArrayList 中间插入元素,需要移动后续所有元素,效率较低。

2. 删除元素 remove(int index)

public E remove(int index) {rangeCheck(index);modCount++;E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0)// 将 index 之后的元素向左移动一位System.arraycopy(elementData, index+1, elementData, index, numMoved);// 将最后一个位置置为 null,方便 GC 回收elementData[--size] = null; return oldValue;
}

结论:删除元素同样需要移动数组,且最后一个元素会被显式设为 null,帮助垃圾回收器回收。

3. 获取与修改 get / set

public E get(int index) {rangeCheck(index);return elementData(index); // 直接通过数组下标访问,O(1)
}public E set(int index, E element) {rangeCheck(index);E oldValue = elementData(index);elementData[index] = element; // 直接替换,O(1)return oldValue;
}

结论:基于数组索引访问,这也是 ArrayList 查询快的根本原因。

五、线程安全问题

ArrayList线程不安全的。在多线程环境下,可能会出现:

  1. 数据覆盖:多个线程同时写入,导致元素丢失。

  2. 并发修改异常:一个线程遍历,另一个线程修改。

  3. JDK 1.7 及之前 HashMap 的死循环问题(虽然 ArrayList 不会死循环,但数据一致性无法保证)。

解决方案

  • 使用 Collections.synchronizedList(new ArrayList<>())(方法级加锁,性能较差)。

  • 使用 CopyOnWriteArrayList(写时复制,读操作完全无锁,适合读多写少的高并发场景)。

写在最后

这是我个人源码阅读系列的第一篇。虽然只是分析了ArrayList,但感觉自己对Java集合的理解比之前扎实了很多。

源码并不可怕,只要带着问题一步步Debug进去,总能发现很多精妙的设计。

如果这篇笔记对你有帮助,点个赞鼓励一下吧~ 有任何疑问也欢迎在评论区一起讨论交流!

http://www.jsqmd.com/news/1399830/

相关文章:

  • 2026 小程序找哪家开发?服务商选型一次讲透 - 互联网转型
  • HarmonyOS应用实战-启示散页-99-发布前日志别靠人工删:用 ReleaseLogAudit 检查白名单
  • Go项目测试质量评估工具对比:为什么go-mutesting是你的最佳选择?
  • 每日学习30
  • Playdate图形设计大师课:1位像素艺术与抖动效果创作指南
  • ComfyUI效率提升300%:ClipProj-MiniMax-H3插件安装与配置教程
  • AI智能体入门到实战:从“会聊天“到“会干活“的一次跃迁
  • 深入理解Make-An-Audio的扩散模型:DDPM与PLMS采样算法原理解析
  • 为什么选择travis-cookbooks?Travis CI环境配置的最佳实践
  • Zotero标签自动化插件怎么用:用Actions Tags把文献管理做成“一条规则“
  • 如何快速搭建Analytics Reporter:从环境配置到首次运行的简明教程
  • ADClusterMapView高级技巧:自定义聚类标注视图与标题的终极方案
  • 3 步上手 RR引导:Redpill Recovery 让闲置 x86 电脑变身群晖 NAS
  • 处置杭州黄金闲置首饰,分清典当质押和直接回收模式 - 日常前沿快讯
  • 安徽电机节能改造:空压机能耗优化方案解析 - 城刊速递
  • 葛仙米种苗批发加工全链测评:藻农生态从基地到餐桌 - 天下观知
  • 二进制安全-Reverse | 底层基础 01 | 从零认识 Reverse:逆向工程研究范畴与学习目标梳理
  • LunaTranslator游戏翻译工具终极指南:三步告别生肉,畅玩日语视觉小说
  • 2026 搭建商城小程序:平台怎么选,避开 90% 商家踩过的坑 - 互联网转型
  • 3 步上手 Awesome Claude Skills:让文献分析与总结工具替你扛下 80% 的阅读量
  • 实战Asmble:Rust代码编译为JVM字节码的完整案例教程
  • Swin Tiny生产环境部署实战:从显存告警到毫秒级推理的完整避坑指南
  • Vue Toast Notification完全指南:Vue.js最优雅的消息提示插件上手教程
  • 10分钟掌握CICFlowMeter流量捕获:从PCAP文件到特征提取全流程
  • 保姆级Legacy-iOS-Kit上手指南:旧iPhone与iPad降级越狱的完整实战教程
  • 武汉葛仙米种苗厂家如何选?藻农生态品控与技术实力解析 - 城刊速递
  • 7天深度体验Claude Code测试能力:它最强的地方不是写测试,是理解整个测试架构
  • register-service-worker核心功能解析:从注册到更新的8个关键事件钩子
  • 深度剖析Playlistor的歌曲匹配机制:如何实现99%精准度的跨平台转换
  • Ember.js与后端集成:RESTful API和GraphQL实战指南