ArrayList源码简析
Posted 热爱编程的大忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList源码简析相关的知识,希望对你有一定的参考价值。
ArrayList源码简析
ArrayList 简介
ArrayList
的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity
操作来增加 ArrayList
实例的容量。这可以减少递增式再分配的数量。
ArrayList
继承于 AbstractList
,实现了 List
, RandomAccess
, Cloneable
, java.io.Serializable
这些接口。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
RandomAccess
是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在ArrayList
中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。ArrayList
实现了Cloneable
接口 ,即覆盖了函数clone()
,能被克隆。ArrayList
实现了java.io.Serializable
接口,这意味着ArrayList
支持序列化,能通过序列化去传输。
Arraylist 和 Vector 的区别?
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用Object[ ]
存储,线程安全的。
Arraylist 与 LinkedList 区别?
先来说说两者的相同点吧:
- 非线程安全
两者的不同点其实就是数组和链表的区别,这里不详细进行展开了,只是概述一番:
- 底层数据结构不同
- 查询,插入和删除元素复杂度不同
- 内存空间占用不同
设计思路
ArrayList本质是一个动态数组的实现,如果要设计一个动态数组,我们需要考虑哪些方面呢?
- 扩容 ! ! ! (扩容是动态数组是否高效的核心)
- 数组默认大小,应该提供接口让用户能够按照业务需求规定初始动态数组的大小,这样可以减少频繁扩容带来的性能损耗。
数组的增删查改没有什么独特优化技巧,无法就是需要在插入前进行扩容判断而已。
ArrayList的核心属性也就是下面两个:
//底层动态数组
transient Object[] elementData;
//动态数组内部元素数量
private int size;
动态数组的长度不等于动态数组里面元素的数量,动态数组的长度称为容量,通常都是容量大于元素数量的。
初始化
ArrayList为我们提供了两种初始化方式,一种是由用户自定规定默认初始化的数组大小,另一种是初始化一个默认大小的数组:
public ArrayList(int initialCapacity)
if (initialCapacity > 0)
//直接初始化一个大小为用户指定大小的数组(这里并没有采用懒加载)
this.elementData = new Object[initialCapacity];
else if (initialCapacity == 0)
//说明用户要初始化一个空数组--对于空数组的表示,ArrayList采用了一个共享空数组变量实例来表示
//实际插入元素时,才会扩容初始化(懒加载)
this.elementData = EMPTY_ELEMENTDATA;
else
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
public ArrayList()
//采用默认初始化方案,初始化一个默认大小的数组,这里也是采用了一个共享实例进行标记
//实际是等到真正插入元素的时候,才会进行初始化(懒加载)
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
这两个共享实例的类型虽说一致,但是含义却不同,核心作用都是进行标记,为了懒加载服务。
private static final Object[] EMPTY_ELEMENTDATA = ;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ;
插入元素
插入元素分为两种情况:
- 直接插入到数组尾部
- 插入到指定索引位置处
public boolean add(E e)
//扩容检查
ensureCapacityInternal(size + 1);
//插入数组尾部
elementData[size++] = e;
return true;
public void add(int index, E element)
//检查Index是否合法--这里大家自行查看源码即可
rangeCheckForAdd(index);
//扩容检查
ensureCapacityInternal(size + 1);
//插入到数组指定位置处,这里需要将index开始的元素都后移一位,然后在index位置处插入当前元素
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
插入元素前需要先进行扩容检查,我们下面来看看。
扩容检查
- 确保当前数组大小能够再塞下n个元素
//这里minCapacity=size+n,add方法传入的n=1,而addAll方法传入的n就不确定是多大了
private void ensureCapacityInternal(int minCapacity)
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
- 处理采用默认大小数组情况下的懒加载问题
private static int calculateCapacity(Object[] elementData, int minCapacity)
//如果动态数组初始化的时候采用的是默认大小,然后构造方法只是打了个标记,还没有进行初始化
//如果下面这个条件满足,说明此时应该是第一次调用add或者addAll方法,size此时等于0
//而minCapacity的大小此时就是等于n,如果调用add方法,那么n=1,否则n是不确定的
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//如果调用的是addAll方法,并且n大于10,那么进行真正初始化的时候,数组大小采用n的大小
//否则在n<10的情况,还是采用默认大小10
return Math.max(DEFAULT_CAPACITY, minCapacity);
return minCapacity;
- 判断当前数组是否可以再放下n个元素,如果可以就不进行扩容
private void ensureExplicitCapacity(int minCapacity)
modCount++;
//minCapacity=size+n
//length=size+数组剩余空闲容量
//下面这个等式等价于: n>数组剩余空闲容量
if (minCapacity - elementData.length > 0)
//扩容
grow(minCapacity);
- 扩容
private void grow(int minCapacity)
//拿到当前数组的容量
int oldCapacity = elementData.length;
//当前数组容器的1.5倍作为新容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断上面通过默认1.5倍扩容方式得到的新容量是否满足当前再插入n个元素的需求
//minCapacity=size+n newCapacity=size+剩余空闲容量大小,因为扩容了,所以空闲容量大小更大了
if (newCapacity - minCapacity < 0)
//如果不满足,一般都是调用了addAll方法,此时新容量就等于minCapacity大小
newCapacity = minCapacity;
//如果数组元素过多直接超过最大限制,那么需要进行处理
//这部分内容比较简单,大家就自行看看源码吧
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//拿到扩容后的新数组
elementData = Arrays.copyOf(elementData, newCapacity);
ensureCapacity
ensureCapacity方法是ArrayList提供的方法,旨在插入大量元素前,用户通过调用该方法提前扩容到对应的大小,避免插入过程中频繁扩容。
public void ensureCapacity(int minCapacity)
//如果当前数组采用默认大小进行初始化的,那么当前用户指定的扩容大小必须要大于默认大小,否则没必要扩容
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;
if (minCapacity > minExpand)
//该方法内部还会再次判断,如果用户指定的扩容大小小于当前数组的容量,那么也没有必要进行扩容操作。
ensureExplicitCapacity(minCapacity);
System.arraycopy() 和 Arrays.copyOf()方法
ArrayList底层的add,remove,grow等涉及到对底层数组具体的操作,都是由上述工具类提供的API来完成的,下面简单介绍一下ArrayList中用到的相关API:
- System.arraycopy() 方法
// 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
/**
* 复制数组
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
- Arrays.copyOf()方法
public static int[] copyOf(int[] original, int newLength)
// 申请一个新的数组
int[] copy = new int[newLength];
// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
Arrays.copyOf()方法主要是为了给原有数组扩容
迭代器
ArrayList本身的源码并没有太多难点,但是容易被大家忽略的是ArrayList内部提供的Iterator,就是因为这个Iterator,才有了令很多人头疼的ConcurrentModificationException并发修改异常出现,下面就来看看Itr迭代器是如何实现的:
private class Itr implements Iterator<E>
//下一个要访问的元素下标---如果构造Itr的时候不传入,默认为0
int cursor;
//上一个要访问的元素下标
int lastRet = -1;
//代表对ArrayList修改次数的期望值,初始值为modCount
int expectedModCount = modCount;
Itr()
//是否还有下一个元素
public boolean hasNext()
return cursor != size;
//获取下一个元素
public E next()
//并发修改检查
checkForComodification();
//获取下一个要访问元素下标
int i = cursor;
//下标越界检查
if (i >= size)
throw new NoSuchElementException();
//拿到当前数组元素集合
Object[] elementData = ArrayList.this.elementData;
//在获取元素期间有其他元素对数组进行了删除操作,会再次产生下标越界,说明出现了并发修改
if (i >= elementData.length)
throw new ConcurrentModificationException();
//下一个要访问元素下标加一
cursor = i + 1;
//通过下标很长访问
//大家思考: 如果是在获取元素其他增加了元素,那么这里通过下标获取到的和一开始期望的就不一致了
//这里侧面说明,ArrayList提供的Itr也非线程安全的
return (E) elementData[lastRet = i];
public void remove()
if (lastRet < 0)
throw new IllegalStateException();
//并发修改检查
checkForComodification();
try
//lastRet在不为-1的情况下,代表着当前元素
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//更新修改次数
expectedModCount = modCount;
//产生越界说明出现并发操作情况
catch (IndexOutOfBoundsException ex)
throw new ConcurrentModificationException();
....
//只有调用了迭代器提供的remove和add方法才会更新expectedModCount的值
//否则可以知道,在使用迭代器遍历当前list的期间,如果直接调用list提供的add和remove方法
//那么便会更新modCount的值,而迭代器这边的expectedModCount并没有被更新
//所以再次通过迭代器获取元素时,就会抛出异常了
final void checkForComodification()
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
private class ListItr extends Itr implements ListIterator<E>
//其他方法不是不重要,而是限于篇幅原因,这里挑出典型讲清楚即可
...
//通过迭代器向集合增加一个元素
public void add(E e)
//并发修改检查
checkForComodification();
try
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
catch (IndexOutOfBoundsException ex)
throw new ConcurrentModificationException();
ArrayList迭代器部分不算特别难,但是通过分析其中典型源码,我们也可以明确使用迭代器时的一些坑,当然大多数情况下,我们都不会直接使用迭代器,而是间接使用它,例如使用增强for循环遍历集合的时候,查看编译过后的java代码可以知道,本质还是利用迭代器进行的遍历,如果我们在增强for循环中对List集合进行add和remove操作,便会抛出ConcurrentModificationException异常:
public class Main
public static void main(String[] args)
ArrayList<Integer> list=new ArrayList<>(20);
list.add(1);
list.add(2);
list.add(3);
for (Integer ele : list)
list.add(1);
解决方法有如下两种:
- 调用迭代器提供的add和remove方法
public static void main(String[] args)
// 创建集合对象
List list = new ArrayList();
// 存储元素
list.add("I");
list.add("love");
list.add("you");
ListIterator lit = list.listIterator();
while (lit.hasNext())
String s = (String) lit.next();
if ("love".equals(s))
// add 、remove 都是可以的
lit.add("❤");
System.out.print(s + " ");
System.out.println();
for (Object l : list)
System.out.print(l + " ");
//运行结果
I love you
I love ❤ you
- 集合遍历元素,集合修改元素(普通for)
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class Demo2
public static void main(String[] args)
//创建集合对象
List list = new ArrayList();
//存储元素
list.add("I");
list.add("love");
list.add("you");
for (int x = 0; x < list.size(); x++)
String s = (String)list.get(x);
if ("love".equals(s))
list.add("❤");
System.out.print(s + " ");
//运行结果
I love you ❤
iterator.remove() 的弊端:
Iterator 只有 remove() 方法,add 方法在 ListIterator 中有
remove 之前必须先调用 next,remove 开始就对 lastRet 做了不能小于 0 的校验,而l astRet 初始化值为 -1
next 后只能调用一次 remove,因为 remove 会将 lastRet 重新初始化为 -1
以上是关于ArrayList源码简析的主要内容,如果未能解决你的问题,请参考以下文章
Android 开发也要懂得数据结构 - ArrayList源码
LinkedList插入数据效率不一定比ArrayList高,源码分析+实验对比