JDK8 ArrayList源码分析
Posted Java软件编程之家
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK8 ArrayList源码分析相关的知识,希望对你有一定的参考价值。
ArrayList类结构图
实现Serializable接口的意义:
如果可序列化类未显式声明serialversionUID,则序列化运行时将计算默认的serialVersionUID值。默认的serialversionUID计算是对类细节高度敏感,这些细节可能因编译器而异,因此可能在反序列化过程中可能导致意外的InvalidClassException。因此,为了在不同Java编译器之间保证一致的序列化版本值,最好为需要序列化的类通过实现Serializable接口来指定一个值。
实现Cloneable接口的意义:
支持对象克隆,源码注释明确说明必须同时实现 java.lang.Object#clone()才是正确的使用方式。
实现RandomAccess接口的意义:
源码注释说明实现该接口时:
for (int i=0, i < list.size(); i++)
循环方式比
for (Iterator i=list.iterator(); i.hasNext(); )
要快。由此说明,使用ArrayList是最佳遍历方式是使用下标索引循环而不是增强for循环或迭代器。
实现Iterable接口的意义:
实现此接口允许对象支持for each loop,增强for循环和迭代器都是基于该接口。
实现Collection接口的意义:
设计为集合实现类的根接口,它的特点是定义集合顶层通用接口方法,包含:
1、返回集合大小接口
2、是否空集合接口
3、某个元素是否存在接口
4、返回普通迭代器接口
5、转换为数组接口
6、新增元素接口
7、删除元素接口
8、批量新增接口
9、批量删除接口
10、Predicate条件删除接口
11、清空集合接口
12、交集接口
13、Stream集合操作接口
14、Spliterator分段迭代器分割符接口
15、Object类的equals和hashCode接口
16、交集判断
对于特殊特性由子接口自己去实现。比如去重、排序、位置查找、位置删除等特性。
实现List接口的意义:
在Collection集合通用接口基础上新增如下特性:
1、排序
2、按位置查找元素
3、元素第一次出现位置查找
4、元素最后一次出现位置查找
5、位置插入
6、位置删除
7、子列表截取
8、支持重复元素
9、特殊双向迭代器(支持从某个位置开始创建迭代器)
10、重写Object类的equals和hashCode接口
实现AbstractCollection抽象类的意义:
对顶层接口Collection的通用实现,这种方式可以减少终端子类的重复实现。实现的接口有:
1、返回集合大小接口
2、是否空集合接口
3、某个元素是否存在接口
4、转换为数组接口
5、删除元素接口
6、交集判断
7、批量新增
8、交集
9、清空
10、toString(我们常用的toString方法就是这里帮处理的)
11、以抛出异常的方式抽象单个新增方法
实现AbstractList抽象类的意义:
对AbstractCollection抽象类进一步覆盖实现和抽象包装,以及对List接口的通用实现。实现List接口有:
1、按位置查找元素
2、元素第一次出现位置查找
3、元素最后一次出现位置查找
4、批量新增
5、普通迭代器接口实现
6、特殊迭代器接口实现
7、子列表截取
8、Object类的equals和hashCode接口实现
9、子类需要的其它代码实现
结构图总结
从ArrayList的类结构图我们得出表面抽象的总结:ArrayList类是个有序的、支持重复元素的集合类,支持元素插入、查找、删除、遍历、序列化、克隆等功能。通常使用增强for循环的效率会比普通下标索引for循环效率要差。
源码分析
源码分析前,简单快速看一下我们平常是如何使用ArrayList的。代码如下:
public static void main(String[] args) {
//new 一个带泛型的ArrayList
List<String> list = new ArrayList<>();
//添加几个元素
list.add("zhangsan");
list.add("lisi");
list.add("wangwu");
//使用增强for循环遍历, 然后做点事情,比如无聊地输出一下
for (String s : list) {
System.out.println("forEach: " + s);
}
//使用普通下标索引for循环遍历, 然后做点事情,比如无聊地输出一下
for (int i = 0; i < list.size(); i++) {
System.out.println("for: " + list.get(i));
}
//删除一个不存在的元素
list.remove("1");
System.out.println(list.size()); //输出3
//删除一个存在的元素
list.remove("zhangsan");
System.out.println(list.size()); //输出2
//删除下标索引为1的元素
list.remove(1);
System.out.println(list.size()); //输出1
//获取某个索引下标的元素
System.out.println(list.get(0)); //输出lisi
}
实例化源码分析
第一步我们先来分析new ArrayList()这行代码都做了什么事情,源码如下:
transient Object[] elementData;
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
哇,我们可以看到,我们天天new ArrayList()的时候,它只是将内部静态空数组引用赋值给了一个内部数组对象elementData,注意:它是Object类型的。我们可以大胆猜想,ArrayList底层就是通过数组来实现的。但是我们知道,数组一旦初始化之后,它的大小是不可变的,而ArrayList是可以不断add的,这又是怎么回事?我们继续往下探索。
add源码分析
接下来我们继续分析下面这行代码,干了些什么事情。
list.add("zhangsan");
进入源码如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
E是泛型参数,我们外层传入的是List<String>,所以我们暂且认为这个泛型参数E类型是String。但实际上这是靠编译器来控制的,一旦编译之后,泛型是会被擦除的。关于泛型我们不过多讲解,继续回到add的源码解析思路来。我们把add涉及的方法和属性都放进来看一下。
//默认空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//底层核心保存数据的数组对象引用
transient Object[] elementData;
//已保存的元素数量,默认初始化为0,每次新增后加1
//其实size代表了两个角色:已保存的元素数量和当前(下一个)元素的索引值
//保存元素前它是当前元素的索引值,保存且size++后就变成下一个元素的索引值
//个人认为如果设立两个属性会直观些,比如eleNum和eleIdx
//另外list.size()方法返回的正是该属性值
private int size;
public boolean add(E e) {
// 每次新增元素都判断是否需要扩容。
// 判断条件:(数组可用大小(length) > (已保存元素数量 + 1))
// + 1是指当前准备待保存的那一个元素e
// 这里体现了size双重角色之已保存的元素角色
ensureCapacityInternal(size + 1);
// 扩容完成后,将新增元素放到size索引位置
// 这里体现了size双重角色之当前元素的索引值角色
elementData[size++] = e;
//表示添加成功
return true;
}
// 初始化扩容大小为10,个人认为命名为INIT_DEFAULT_CAPACITY更直观些
private static final int DEFAULT_CAPACITY = 10;
private void ensureCapacityInternal(int minCapacity) {
//是否初始化
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 设置初始化扩容为10
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 初始化校验后,继续扩容逻辑
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//这是AbstractList类属性,这个字段留给子类决定是否使用
//迭代器时可借此判断数组是否有其它线程对数组进行元素新增和删除等操作。
//个人认为命名为modifyVersion比较直观些
modCount++;
// 这里真正决定是否需要扩容
// minCapacity定义为数组可用大小不够放下新的元素或不满足初始化大小要求
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//整个数组大小最大限制不能超过int类型数值范围
//减去8是因为虚拟机保留数组头关键字,不预留可能会发生OOM异常
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
private void grow(int minCapacity) {
// 数组可用大小
int oldCapacity = elementData.length;
// 计算新的容量大小为:数组可用大小 + 数组可用大小除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 传入参数(10或当前元素数量)和计算得到新容量对比,取最大值
if (newCapacity - minCapacity < 0)
// 在new ArrayList()初始化时就会为true。
newCapacity = minCapacity;
// 控制不能超过int类型数值范围-8
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 这里最后是通过底层native原生方法进行数组复制扩容
// 按新的容量创建一个新的数组,把旧的数组数据转到新数组中,返回新数组
// 这个方法比较重要,用的地方比较多,下面会通过文自说明一下
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
简答解释下上面多提提到的数组可用大小和已保存的元素数量,举个例子:
String[] s = new String[10];
s[0] = "111";
s[1] = "222";
s[2] = "333";
上面数组s的可用大小(length)是10,已保存元素数量(size)为3;
在ArrayList源码中,很多地方都用到该方法:Arrays.copyOf(elementData, newCapacity),这里将其独立拿出来通过一个例子演示下,读者很快就会理解了。
public static void main(String[] args) {
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, 5);
int[] arr3 = Arrays.copyOf(arr1, 10);
int[] arr4 = Arrays.copyOf(arr1, 2);
for(int i = 0; i < arr2.length; i++)
System.out.print(arr2[i] + " ");
arr2[0] = 100;
System.out.println();
for(int i = 0; i < arr3.length; i++)
System.out.print(arr3[i] + " ");
System.out.println();
for(int i = 0; i < arr4.length; i++)
System.out.print(arr4[i] + " ");
System.out.println();
for(int i = 0; i < arr1.length; i++)
System.out.print(arr1[i] + " ");
}
输出:
1 2 3 4 5
1 2 3 4 5 0 0 0 0 0
1 2
1 2 3 4 5
总结:根据第二个参数作为长度新创建一个数组,从原数组复制对应索引到新数组,如果新数组长度大于原数据,则用0填充,如果新数组长度小于原数据,则截止到指定位置。
到这里add源码分析就结束了,我们总结下add大概会做以下事情:每次新增都会判断数组可用大小不够放下新的元素或不满足初始化大小要求。如果足够则不扩容,否则需要扩容,每次扩容大小计算如下:初始化扩容大小为10,后面每次扩容大小为原数组大小加原数组大小的一半但不得超过int类型的最大数值范围。现在我们知道了如果要保存11个元素且如果使用了new ArrayList()的方式,最后该list的内部开辟的数组大小空间占用是10 + 10 / 2=15。其中4个空位置是没有数据的,而且进行了一次数组复制开销。如果不是11,而是更大的数量100001,刚好也是多一个元素,这次开辟数据大小可能就是150000。大名鼎鼎的ArrayList真的这么傻吗?
其实ArrayList不会这么傻,而是你懒惰不学习。不信,你看ArrayList的另一个构造器:
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);
}
}
所以,看完Add方法后,我们又总结一条,在new ArrayList()时,尽可能指定实际需要的容量大小。如果无法准确知道大小,预估一个大一点单不能大太多的容量值也是不错的方式,至少减少了数组复制的开销。
迭代源码分析
接下来我们继续分析下面这行代码,干了些什么事情。
for (String s : list) {
System.out.println("forEach: " + s);
}
糟糕,这个源码怎么看呢?可能我们还记得这是增强for循环,但是可能已经忘了增强for循环是java的语法糖,语法糖意思就是为了使用方便而设计的一种语法,这种语法经过编译器编译后会被解析还原为语法糖背后的代码。下面是编译后的代码:
Iterator var3 = list.iterator();
while(var3.hasNext()) {
String s = (String)var3.next();
System.out.println("forEach: " + s);
}
我们把它复制到代码上,替换掉语法糖,就可以进入源码分析了,源码如下:
public Iterator<E> iterator() {
return new Itr();
}
//Itr是ArrayList私有内部类,它实现了Iterator接口
//我们每天使用ArrayList的迭代器或增强for循环正是由它支撑的
private class Itr implements Iterator<E> {
//要返回的下一个元素的索引,首次初始化为0,每次next后+1
int cursor;
//返回的最后一个元素的索引;-1表示没有
int lastRet = -1;
//记录当前数组修改版本号
int expectedModCount = modCount;
public boolean hasNext() {
//要返回的下一个元素的索引不等于当前数组元素数量,就认为还有元素
//size是外部类字段,记得前面提过的size双重角色
return cursor != size;
}
"unchecked") (
public E next() {
//每次获取下一个值时检查版本号是否被其它线程修改过
checkForComodification();
//cursor初始化为0 i=0
int i = cursor;
//校验当前要返回的元素的索引是否大于等于当前数组元素数量
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//校验索引是否超过数组可用大小
if (i >= elementData.length)
throw new ConcurrentModificationException();
//加1作为下一个元素的索引
cursor = i + 1;
//获取值,然后将当前元素索引赋值给lastRet作为返回的最后一个元素的索引
return (E) elementData[lastRet = i];
}
//检查数组版本号
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
//迭代器对象删除元素方法,迭代器如果要删除某个元素,只能通过迭代器对象自己的remove方法进行删除,不能通过ArrayList对象的reomve方式。因为这里会重新计算版本号以及索引值
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//调用ArrayList的remove
ArrayList.this.remove(lastRet);
//重新计算值,保证迭代器正常继续工作
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//对每个剩余元素执行给定操作,直到所有元素已处理或操作引发异常
//一旦迭代过程中调用该方法后,下次hasNext返回的就是false
"unchecked") (
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
}
到这里迭代器源码分析就结束了,我们对ArrayList迭代器相关总结如下:
1、增强for循环本质上就是通过迭代器实现
2、ArrayList通过私有内部类Itr实现迭代器接口
3、通过interator方法返回Itr迭代器对象
4、迭代器过程中只能使用迭代器对象自己的remove方法来删除元素
5、迭代器迭代过程不支持对数组的并发操作
remove源码分析
接下来我们继续分析下面这行代码,干了些什么事情。
list.remove("lisi");
ArrayList可以通过索引来删除,也可以通过元素引用来删除元素,这点实现方式是不一样的。下面我们先看一下通过元素引用来删除的源码:
public boolean remove(Object o) {
//通过源码可以看到,ArrayList是支持保存null的
//因为null不用调用equals,所以这里搞了个if else来分别处理
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
//很简单,就是遍历数组,通过equals来找到删除的对象
//所以说equals和hasCode方法对于集合来说是很重要的
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
//删除工作由这个方法实现,把元素索引传过去
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
//修改版本号
modCount++;
// 计算要移动的数量(数组元素数量 - 待删除元素的索引 - 1)
// 将待删除元素索引后面索引的元素往后移动
int numMoved = size - index - 1;
if (numMoved > 0)
//这个方法很重要,我们通过下面文字单独进行讲解一下
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 移动后把最后元素置为null,下次GC时可直接回收被删除的元素对象
elementData[--size] = null;
}
System.arraycopy源码如下:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
通过源码可以看到它是一个native方法,这种方法一般在native包下相同类型的C语言实现,这里我们不再深入研究,我们只了解接口参数使用即可,参数解释如下:
src : 原数组srcPos : 原数组的起始位置dest : 目标数组destPos : 目标数组的起始位置length : 要copy的长度
回到代码配合例子图分析如下:
System.arraycopy(elementData, index+1, elementData, index, numMoved)
原数组:elementData
原数组的起始位置:index+1=3 (4)
目标数组: elementData
目标数组的起始位置:index=2 (3)
要copy的长度: size - index -1 =2 (4、5)
实际上就是把待删除的元素索引后面的索引往后移,把4放到3位置,5放到4位置。
分析完根据元素对象进行删除,我们可以快速理解根据索引进行删除的代码:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
E elementData(int index) {
return (E) elementData[index];
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
读者自己应该都可以快速理解上面源码的大致流程吧。
到这里remove源码分析就结束了,我们对ArrayList remove相关总结如下:
1、remove操作不会改变原数组长度
2、remove操作不会新创建数组
3、remove操作会修改版本号
4、remove通过索引方式比元素对象方式效率要高很多
5、remove后面均会被置为null
get源码分析
接下来我们继续分析下面这行代码,干了些什么事情。
list.get(0)
源码如下:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
return (E) elementData[index];
}
我们有了上面源码分析的基础,我觉得这段源码不用再讲解。
其它方法源码分析
指定索引新增:
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
根据索引新增源码涉及的方法我们前面都讲解过,所以这里不再重复讲解。
我们前面提到ArrayList是支持批量操作地,我们来看下源码是如何实现的
批量新增:
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
批量删除:
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
//这里说明下,由于交集也调这个方法,通过传不同的complement来实现复用
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 {
if (r != size) {
// 只有当上面代码抛出异常时,才可能进入这里
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// w表示没有被删除剩下元素最大索引
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
核心的方法System.arraycopy和Arrays.copyOf我们已经讲解过。相信看到这里对于ArrayList的源码应该有个大致理解。
源码总结
虽然我们并没有把ArrayList所有方法都分析一遍,但是整体内容已经足够了解ArrayList的核心原理。我们下面简单总结下如何高效和正确地使用ArrayList。
1、创建ArrayList对象时,尽可能评估好本次使用的容量,使用带容量参数的构造器实例化
2、ArrayList随机访问效率等于数组索引访问的速度,效率很高
3、当评估好本次使用的容量,使用带容量参数的构造器实例化时,ArrayList顺序新增的效率很高
4、往ArrayList中间位置新增效率较低,因为要移动数组,如果业务上需要频繁调用的话,应该考虑使用LinkedList集合类
5、ArrayList删除操作效率较低,最坏情况是删除第一个元素,如果业务上需要频繁调用的话,应该考虑使用LinkedList集合类
6、对于ArrayList而言,优先使用索引下标方式的for循环,而不是迭代器或增强foreach
7、在迭代器过程中,如果需要删除元素,必须使用迭代器对象的remove方法,而不是ArrayList对象的remove方法
8、ArrayList是线程不安全的类,业务代码需要自己处理并发安全问题
-END-
以上是关于JDK8 ArrayList源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Java ArrayList底层实现原理源码详细分析Jdk8