作者多隆(企业代号名),目前负责贝壳基础技术中心不动产数据标准化业务,专注java后台技术、代码设计。
一、 综述
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的源码实现,但是能力有限,不能保证分毫不错,面面俱到。因此,希望本文能够抛砖引玉,给大家更多的启发,引导大家多到底层看看。
四、 彩蛋
其实这不是一个彩蛋,而是自己在阅读集合类源码经常碰到的一个问题。ArrayList继承了AbstractList并实现了List接口,但是AbstractList也实现了List接口,jdk为什么要这样设计呢?
参考文章:
1、 ArrayList集合源码
https://www.cnblogs.com/zhangyinhua/p/7687377.html
2、 深入分析Java的序列化与反序列化
https://www.hollischuang.com/archives/1140
作 者: 多隆(企业代号名)
出品人:辛白、辰旭(企业代号名)
---------- END ----------