一、综述
1.1 简介
ArrayList 是一个大小可以调整的动态数组,适应于查询为主的场景(备注:对应删除为主的是 LinkedList),并提供了添加、删除、更改、遍历的方式。
ArrayList 不是一个线程安全的集合。并发修改时,可能会抛出 ConcurrentModificationException 或者得到无法预料的结果。因此如果并发处理,要么更换线程安全的集合,要么依赖线程安全机制去保证 ArrayList 的并发处理。
1.2 继承关系
ArrayList
描述下各个抽象类、接口的作用:
RandomAccess 是一个标记接口,用于标记实现该接口的集合支持快速随机访问。
Serializable 是一个标记接口,用于标记实现该接口的类可以序列化。
Cloneable 是一个标记接口,用于标记实现该接口的类可以调用 clone 方法,否则会抛异常。
Iterable 是一个遍历接口,内部提供了支持不同遍历方式的方法,比如顺序遍历迭代器、函数式的 foreach 遍历、并行遍历迭代器。
Collection 是 java 集合体系的根接口,包含了通用的遍历、修改方法,例如 addAll、removeAll。
AbstractCollection 是一个抽象类,重写了 Collection 中最基础的方法,减少具体集合类的实现成本,比如 contains、isEmpty、toArray,iterator,但是 add 等需要具体集合类自我实现。
List 是 java 有序集合的基础接口,除了 Collection 的方法,还有支持倒序遍历的 listIterator 方法、子列表 subList 方法,另外重写 spliterator 方法的实现。
AbstractList 是一个抽象类,重写了 List 的大部分方法,作用跟 AbstractCollection 类似。
二、剖析
剖析以源码注释为主,以流程图为辅,解释 ArrayList 的字段定义、方法实现与设计思路。内容包含 ArrayList 的字段、构造、修改、遍历、序列化、线程安全六大部分,下面一一详解。
2.1 字段
/**
* 序列化版本标识,序列化和反序列化时使用
*/
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认的数据容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于ArrayList空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用于默认大小空实例的共享空数组实例。
* 我们将DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA区别开来
* 以便在添加第一个元素时知道要膨胀多少。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存放元素的数组
* 备注:字段不设置为私有,是为了方便内部类的访问
* 思考:为什么不是E[]呢?
*/
transient Object[] elementData;
/**
* 数组元素个数
*/
private int size;
2.2 构造方法
/**
* 1、创建ArrayList强制使用范型,避免使用原生类型引起类型不安全的问题
* 2、java7之后的jdk增强了类型推导,建议使用new ArrayList<>(),最好不使用new ArrayList<E>
*/
// 创建一个特定长度的ArrayList
// 如果可以预估容量,请使用本方法构建实例,避免扩容时数组拷贝带来的性能消耗
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果容量为0,则都指向同一个共享的空数组
// 减少内存的占用
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 创建一个容量为10的ArrayList
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 使用Collection的实现比如Set,List创建一个ArrayList
// 通常是Collection的实现进行相互转换
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray返回类型不一定Object[],具体见https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-6260652
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 使用空数组替换
this.elementData = EMPTY_ELEMENTDATA;
}
}
2.3 添加
添加可以分为两种:单个添加(添加特定元素)、批量添加(添加集合)。
单个添加
流程
/**
* 在ArrayList结尾添加元素
*/
public boolean add(E e) {
// 根据size处理容量
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 如果使用ArrayList()创建,默认容量=DEFAULT_CAPACITY=10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// minCapacity为此时elementData必须的最小长度
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
// 修改次数+1,用于fail-fast处理
modCount++;
// 如果minCapacity大于elementData的长度,则进行扩容处理
if (minCapacity - elementData.length > 0)
// 扩容,可能会引起溢出问题
grow(minCapacity);
}
// ArrayList动态扩容机制的核心
private void grow(int minCapacity) {
// 可能存在整型溢出
int oldCapacity = elementData.length;
// 容量默认扩大1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 可能1:newCapacity<0整型溢出
// 可能2:newCapacity<minCapacity
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 数组深拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
// 说明已经整型溢出
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
/**
* 在ArrayList特定位置添加单个元素
* 思考:add(E e)没有调用add(int index, E element),个人猜测是出于性能的考虑
* 毕竟基于数组进行插入操作可能存在性能问题
*/
public void add(int index, E element) {
// 检查位置是否合法
rangeCheckForAdd(index);
// 跟add(E e)中处理方式类似
ensureCapacityInternal(size + 1);
// 将elementData中位置为index位置及其后面的元素都向后移动一个下标(底层是native方法,使用cpp直接操作内存。)
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
批量添加
//
public boolean addAll(Collection<? extends E> c) {
// 集合转化成数组
Object[] a = c.toArray();
int numNew = a.length;
// 跟add(E e)中处理方式类似
ensureCapacityInternal(size + numNew);
// 将集合内的元素复制到elementData中,覆盖[size, size+numNew)的元素
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
// 检查位置是否合法
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
int numMoved = size - index;
if (numMoved > 0)
// 将elementData中位置为index及其以后的元素都向后移动numNew个位置
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
// 将集合内的元素复制到elementData中,覆盖[index, index+numNew)的元素
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
2.3 删除
删除可以分为两种:单个删除(删除特定元素、特定下标)、批量删除(删除集合中的元素)、批量保留(批量删除除集合外的元素)、清空。
单个删除
// 删除ArrayList中第一次出现的特定元素
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
// 比较对象时依赖equals方法
// 因此类型变量E对应的类注意重写equlas方法
// 重写时注意遵守规范,具体参考effective java第三版的第10、11两条规则
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
// 根据下标删除元素
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
// 将elemenData中index+1及其后面的元素都向前移动一个下标
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 根据上一步的操作, size-1位置的对象向前移动了一个下标
// 如果没有elementData[--size]==null,可能会导致内存泄漏
// 试想,ArrayList被add了100个对象,然后被remove了100次。按照GC的机制来说,100个对象应该可以被GC掉(假设没有对象对象),但是由于还存在ArrayList的实例引用,对应的100个对象就无法删除
elementData[--size] = null;
}
// 根据下标删除元素
// 注意:java5后引入自动装箱、拆箱的机制,因此产生了一个有趣的问题:
// 当类型变量为Integer的ArrayList调用remove时,可能调用remove(Object),也可能调用remove(Index)
// 一定要注意测试是否符合自己的预期
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
// 如果被删除元素不是ArrayList的最后一个元素
if (numMoved > 0)
// 对应下标之后的元素向前移动一个下标
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 最后一个元素只为null,方便GC
elementData[--size] = null;
return oldValue;
}
批量删除
// 批量删除ArrayList和集合c都存在的元素
public boolean removeAll(Collection<?> c) {
// 非空校验
Objects.requireNonNull(c);
// 批量删除
return batchRemove(c, false);
}
private boolean batchRemove(Collection<?> c, boolean complement){
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
// 把需要保留的元素前置
elementData[w++] = elementData[r];
} finally {
// 即使c.contains抛异常,也要保持跟AbstractCollection行为的兼容性
// 备注:ArrayList重写了AbstractCollection中的removeAll方法,removeAll调用了batchRemove
if (r != size) {
// 备注1:可能是上面的for循环出现了异常
// 备注2:可能是其它线程添加了元素。
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
for (int i = w; i < size; i++)
// 跟fastRemove(int index)里面的操作类似,防止内存泄漏
elementData[i] = null;
// 思考:为什么addAll的modCount+1,而removeAll的modCoun+size-w
// 个人以为modCount只是做标记做了结构的修改并且用来做校验。
// 因此+1,+2 +size-w并没有本质区别
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
// 思考:上面是按照元素进行批量删除,如何按照下标区间进行批量删除呢?
批量保留
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
// 批量保留
return batchRemove(c, true);
}
清空
public void clear() {
modCount++;
// 清空ArrayList里面所有的元素
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
更改
// 修改特定下标的值
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
查找
// 返回元素第一次出现的下标
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
// 返回最后一次出现的位置
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
遍历方式
ArrayList 可以通过 for、foreach、foreach-lambda、iterator 进行遍历,iterator 又可以分为 Iterator(只能按照 index 从 0 到 size 进行遍历)、ListIterator(可以按照 index 从小到大进行遍历,也可以从大到小进行遍历)、Spliterator(并行遍历,充份发挥多核优势),下面依次进行演示。
List<String> strList = new ArrayList<String>(4);
strList.add("1");
strList.add("2");
strList.add("3");
// for遍历
for (int i = 0; i < strList.size(); i++) {
System.out.println(strList.get(i));
}
// foreach
// 备注:只要实现Iterable接口,就能使用foreach
for (String s : strList) {
System.out.println(s);
}
// foreach-lambda遍历
strList.forEach(System.out::println);
// iterator遍历
Iterator<String> iterator = strList.iterator();
while (iterator.hasNext()){
String str = iterator.next();
System.out.println(str);
// 下一次出现ConcurrentModificationException
// 问题是因为list的iterator方法返回的是ArrayList的内部类Itr
// Itr里面的expectedModCount会与ArrayList的modCount进行比较
// 剩下的就不言而喻
strList.remove(str);
// 进行iterator遍历时,如果进行remove等操作,调用iterator的方法
// 而不是ArrayList的方法
// iterator.remove();
}
// ListIterator可以进行顺序、逆序遍历,可以指定index位置开始遍历
ListIterator<String> iterator = strList.listIterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
iterator = strList.listIterator(strList.size());
while (iterator.hasPrevious()){
System.out.println(iterator.previous());
iterator.remove();
}
// 使用并行遍历,可以将元素放在不同的迭代器进行并行遍历
Spliterator<String> spliterator = strList.spliterator();
// split,分而治之,类似算法里面的分治
Spliterator<String> otherSpliterator = spliterator.trySplit();
spliterator.forEachRemaining(System.out::println);
otherSpliterator.forEachRemaining(System.out::println);
序列化
ArrayList 的序列化没有直接序列化 elementData,而是根据 size 序列化包含的元素,忽略数组中的其它位置,提高效率并节省空间。
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// fail-fast,后续判断是否有并发处理
int expectedModCount = modCount;
// 序列化没有标记为static、transient的字段,包括size等。
s.defaultWriteObject();
// 没有意义,可以忽略
s.writeInt(size);
// 序列化元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
// ArrayList被并发处理,发生结构性修改
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// 反序列化没有标记为static、transient的字段,包括size等
s.defaultReadObject();
// 可以忽略,跟writeObject里面的方法对应
s.readInt();
if (size > 0) {
// 数组扩容
ensureCapacityInternal(size);
Object[] a = elementData;
// 反序列化元素并填充到数组中
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
排序
List<String> strList = new ArrayList<String>(4);
strList.add("1");
strList.add("2");
strList.add("3");
// 可以使用以下三种排序方式
Collections.sort(strList);
Collections.sort(strList, String::compareTo);
strList.sort(String::compareTo);
//java8 新增的排序方法
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
// 底层使用合并排序算法进行排序
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
转化数组
public Object[] toArray() {
// 直接复制ArrayList的elementData
return Arrays.copyOf(elementData, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
// 利用反射生成特定类型的数组并复制
// 备注:但是不知道为什么toArray的类型变量T跟ArrayList的不一致
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// 另外,除了根据ArrayList转化成数组,同样可以根据Arrays的asList将数组转换成List
// 备注:Arrays是数组操作的util类,可以进行排序、查找、复制、遍历等
// 注意:asList方法返回的私有静态内部类ArrayList,静态内部类ArrayList跟java.util.ArrayList不同
// 注意:静态内部类ArrayList没有重写java.util.AbstractList的remove、add等方法,默认实现是直接抛UnsupportedOperationException,因此调用会报错
List<String> strList = Arrays.asList("1", "2", "3");
线程安全
开头说过,ArrayList 并不是线程安全的集合,源码剖析也展示了 ArrayList 通过 modCount 以及 fali-fast 机制去避免一定程度的线程安全问题,那么我们如何保证 ArrayList 的线程安全呢?其实,我们可以通过以下方案实现:
使用 Vector 代替 ArrayList。
使用 Collections.synchronizedList 包装 ArrayList,然后操作包装后的 list 即可。
使用 CopyOnWriteArrayList 代替 ArrayList。
在使用 ArrayList 时,应用程序通过同步机制去控制 ArrayList 的读写,不建议。
前面提过,ArrayList 是一个查询为主的数据结构,本身不适合修改频繁以及并发修改的场景。如果需要并发操作,可以使用上面的方案,但是它们都会有一定的瓶颈,或许我们更换其它的集合类更合适,比如线程安全的队列。
三、总结
前面通过注释剖析了 ArrayList 的源码实现,但是能力有限,不能保证分毫不错,面面俱到。因此,希望本文能够抛砖引玉,给大家更多的启发,引导大家多到底层看看。
作者介绍:
多隆(企业代号名),目前负责贝壳基础技术中心不动产数据标准化业务,专注 java 后台技术、代码设计。
本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。
原文链接:
https://mp.weixin.qq.com/s/dy98Y-LyQw1BbbH3_X7nBw
更多内容推荐
LinkedList 源码分析 - 迭代器
LinkedList中使用的迭代器是:ListIterator(Iterator 只支持从头到尾的访问),这个迭代器提供了向前和向后的迭代方法。
2022-05-21
Java 集合(3)-- iterable 接口超级详细解读
iterable接口其实是java集合大家庭的最顶级的接口之一了,实现这个接口,可以视为拥有了获取迭代器的能力。Iterable接口出现在JDK1.5,那个时候只有iterator()方法,主要是定义了迭代集合内元素的规范。
2020-11-16
java 集合【10】——— LinkedList 源码解析
我们除了最最常用的ArrayList之外,还有LinkedList,这到底是什么东西?从LinkedList官方文档,我们可以了解到,它其实是实现了List和Queue的双向链表结构,而ArrayList底层则是数组结构。
2020-12-05
Java AQS 核心数据结构 -CLH 锁
在并发编程中,锁是一种常用的保证线程安全的方法,CLH 锁是对自旋锁的一种改良,本文对此进行介绍。
并发王者课 - 铂金 2:豁然开朗 -“晦涩难懂”的 ReadWriteLock 竟如此妙不可言
欢迎来到《并发王者课》,本文是该系列文章中的第15篇。 在上篇文章中,我们介绍了Java中锁的基础Lock接口。在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWriteLock接口。
2021-06-17
Java 集合(2)-- Iterator 接口超级详细解读
iterator接口,也是集合大家庭中的一员。和其他的Map和Collection接口不同,iterator 主要是为了方便遍历集合中的所有元素,用于迭代访问集合中的元素,相当于定义了遍历元素的规范,而另外的Map和Collection接口主要是定义了存储元素的规范。
2020-11-16
03|依赖注入:如何给 Bean 注入值并解决循环依赖问题?
继续手写MiniSpring,探讨Bean的依赖注入
2023-03-17
面试侃集合之 DelayQueue 篇
上一篇文章我们聊完了PriorityBlockingQueue,今天我们再来聊聊和它相关的DelayQueue吧。
2022-04-02
06|再回首:如何实现一个 IoC 容器?
实现一个IoC容器
2023-03-24
02|扩展 Bean:如何配置 constructor、property 和 init-method?
配置constructor、property和init-method
2023-03-15
15|Wrapper 机制:Wrapper 是怎么降低调用开销的?
今天,我们对在提供方服务设计统一入口来接收各种请求的案例进行分析与改造。
2023-01-20
LinkedList 源码分析 - 删除
节点删除的方式和节点追加类似,可以从头部删除,或从尾部删除。
2022-05-20
01|原始 IoC:如何通过 BeanFactory 实现原始版本的 IoC 容器?
用原始的IoC容器来管理一个Bean
2023-03-13
在 Java 中如何加快大型集合的处理速度
对于某些流数据集,并行处理可能比串行处理更慢。
Java 工程师该如何编写高效代码?
本文介绍Java高效代码50例。
Java 集合(4)-- iterable 和 iterator 异同分析
iterator接口,也是集合大家庭中的一员。和其他的Map和Collection接口不同,iterator 主要是为了方便遍历集合中的所有元素,用于迭代访问集合中的元素,相当于定义了遍历元素的规范,而另外的Map和Collection接口主要是定义了存储元素的规范。
2020-11-16
【Howe 学 JAVA】Java 类集框架 2——集合输出
Collection 接口中的 toArray() 方法可以将集合保存的数据转为对象数组返回,用户可以利用数据循环的方式获取内容。但是此类方式由于性能不高并不是集合输出的首选方案。在类集框架中对于集合的输出提供了 4 种方式:
2020-05-11
Java 锁的那些事儿
本文主要分析Java锁机制的使用和实现原理,按照Java锁使用、JDK中锁实现、系统层锁实现的顺序来进行分析。
JUC 浅析(一)
支持多线程操作的各种语言,尤其是Java,一定会使用到多线程。在访问资源时,多线程非常重要,对资源的安全起到很好的作用。要想实现多线程处理操作,需使用Object类中的wait()、notify()、notifyAll()方法以及关键字synchronized,这在生产者与消费者设计模
2022-10-28
04|增强 IoC 容器:如何让我们的 Spring 支持注解?
实现Autowired注解,并用这个方式进行依赖注入
2023-03-20
推荐阅读
9、源码阅读之 mapper 代理底层源码
2023-09-28
详解 ConCurrentHashMap 源码(jdk1.8)
2022-12-01
加入有序集合,Java 集合框架变得更加完善
15|mBatis:如何将 SQL 语句配置化?
2023-04-14
30|函数式语法糖:如何使用 Function 、Stream 来编写函数式程序?
2023-11-06
JAVA concurrency -- AQS 源码详解
2022-11-16
ArrayList 源码解析
2022-11-02
电子书
大厂实战PPT下载
换一换 夏巨鹏(真谛) | 蚂蚁集团 大安全技术 资深安全专家
杨海银 | 美的 IoT 软件开发移动总架构师
许辰 | 字节跳动 高级工程师
评论