从源码角度彻底搞懂ArrayList
天天使用的ArrayList,今天咱们从深层次的源码角度来分析分析。...
今日科技快讯
近日,富士康工业互联网股份有限公司的招股书在证监会网站预披露,拟在上交所上市。国内科技类最大公司是海康威视3400多亿,360借壳成功也是3400多亿,富士康若是能上市成功,很可能成为A股市值最高的科技股龙头。作者简介
大家周一早上好!新的一周又开始了,继续整理好新的心情加油吧!本篇来自 潇风寒月 的投稿,分享了从源码的角度来分析了ArrayList,一起来看看!希望大家喜欢。
潇风寒月 的博客地址:
https://blog.csdn.net/xfhy_
前言
学Java很久了,一直处于使用API+查API的状态,不了解原理,久而久之总是觉得很虚,作为一名合格的程序员这是不允许的,不能一直当API Player,我们要去了解分析底层实现,下次在使用时才能知己知彼.知道在什么时候该用什么方法和什么类比较合适.
ArrayList基本特点
- 快速随机访问
- 允许存放多个null元素
- 底层是Object数组
- 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),改对应索引元素比较快
ArrayList的继承关系
来看下源码中的定义
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
- 可以看到继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作.对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类.
- 实现了List接口,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合.
- 实现了RandomAccess接口标识着其支持随机快速访问,实际上,我们查看RandomAccess源码可以看到,其实里面什么都没有定义.因为ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1).
- 实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制(不知道此概念的赶快去查资料,这知识点非常重要).
- 实现了Serializable 标识着集合可被序列化。
ArrayList 的构造方法
在说构造方法之前我们要先看下与构造参数有关的几个全局变量:/**
* ArrayList 默认的数组容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 另一个共享空数组实例,用的不多,用于区别上面的EMPTY_ELEMENTDATA
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* ArrayList底层的容器
*/
transient Object[] elementData; // non-private to simplify nested class access
//当前存放了多少个元素 并非数组大小
private int size;
注意到,底层容器数组的前面有一个transient关键字,啥意思??
查阅资料后,大概知道:transient标识之后是不被序列化的
但是ArrayList实际容器就是这个数组为什么标记为不序列化??那岂不是反序列化时会丢失原来的数据?其实是ArrayList在序列化的时候会调用writeObject(),直接将size和element写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取size和element,再恢复到elementData。
原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
无参构造方法
/**
* 构造一个初始容量为10的空列表。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
命名里面讲elementData指向了一个空数组,为什么注释却说初始容量为10。这里先卖个关子,稍后分析。
指定初始容量的构造方法
public ArrayList(int initialCapacity) {
//容量>0 -> 构建数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//容量==0 指向空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//容量 c) {
//判空
Objects.requireNonNull(c);
return batchRemove(c, false);
}
//complement是true 则移除elementData中除了c以外的其他元素
//complement是false 则移除c和elementData(当前列表的数组)都含有的元素
private boolean batchRemove(Collection c, boolean complement) {
//1. 引用不可变
final Object[] elementData = this.elementData;
//r 是记录整个数组下标的, w是记录有效元素索引的
int r = 0, w = 0;
boolean modified = false;
try {
//2. 循环遍历数组
for (; r < size; r++)
//3. 如果complement为false 相当于是取c在elementData中的补集,c包含则不记录,c不包含则记录
//如果complement为true 相当于是取c和elementData的交集,c包含则记录,c不包含则不记录
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r]; //r是正在遍历的位置 w是用于记录有效元素的 在w之前的全是有效元素,w之后的会被"删除"
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
//4. 如果上面在遍历的过程中出错了,那么r肯定不等于size,于是源码就将出错位置r后面的元素全部放到w后面
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
//5. 如果w是不等于size,那么说明是需要删除元素的 否则不需要删除任何元素
if (w != size) {
// clear to let GC do its work
//6. 将w之后的元素全部置空 因为这些已经没用了,置空方便GC回收
for (int i = w; i < size; i++)
elementData = null;
modCount += size - w;
//7. 记录当前有效元素
size = w;
//8. 标记已修改
modified = true;
}
}
return modified;
}
大体思路:
- 首先我们进行c集合检查,判断是否为null
- 然后我们调用batchRemove()方法去移除 c集合与当前列表的交集
- 循环遍历当前数组,记录c集合中没有的元素,放在前面(记录下标为w),w前面的是留下来的元素,w后面的是需要删除的数据
- 第3步可能会出错,出错的情况下,则将出错位置的后面的全部保留下来,不删除
- 然后就是将w之后的元素全部置空(方便GC回收),然后将size(标记当前数组有效元素)的值赋值为w,即完成了删除工作
- 清空列表
这个方法非常简单,就是将数组所有元素都置为null,然后GC就有机会去把它回收了
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData = null;
size = 0;
}
移除相应区间内的所有元素(protected)
//这是protected方法 移除相应区间内的所有元素
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
//1. toIndex后面的元素需要保留下来,记录一下toIndex后面的元素个数
int numMoved = size - toIndex;
//2. 将toIndex后面的元素复制到fromIndex处
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
//3. 将有效元素后面的元素置空
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData = null;
}
//4. 记录当前有效元素个数为size - (toIndex-fromIndex) ,即减去那个区间内的元素个数
size = newSize;
}
大体思路:
- 假设需要移除(fromIndex,toIndex)区间内的元素,那么将toIndex后面的元素复制到fromIndex处
- 将有效元素后面的元素置空
改动元素
替换指定下标的元素内容
public E set(int index, E element) {
//1. 入参检测
rangeCheck(index);
//2. 记录原来该index处的值
E oldValue = elementData(index);
//3. 替换
elementData[index] = element;
return oldValue;
}
查询元素
返回指定位置处元素
E elementData(int index) {
return (E) elementData[index];
}
/**
* 返回指定位置处元素
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
通过iterator()遍历
首先我们了解一下fail-fast,fail-fast 机制是java集合(Collection)中的一种错误机制。这也是查询的一种,哈哈
当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。
我们先来看看iterator()方法,它new了一个Itr(ArrayList的内部类)进行返回.
/**
* Returns an iterator over the elements in this list in proper sequence.
* The returned iterator is fail-fast.
* @return an iterator over the elements in this list in proper sequence
以适当的顺序返回此列表中元素的迭代器。 fail-fast:快速失败?
*/
public Iterator iterator() {
return new Itr();
}
接下来我们来看看这个内部类
[code]private class Itr implements Iterator {
int cursor; // index of next element to return 下一个元素的索引
int lastRet = -1; // index of last element returned; -1 if no such 当前访问的最后一个元素的索引
int expectedModCount = modCount;
//是否有下一个元素
public boolean hasNext() {
//就是比一下cursor与size的大小 但是为什么是!=,而不是cursor= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//不能访问超出elementData.length的索引 可能是被其他线程改动了
if (i >= elementData.length)
throw new ConcurrentModificationException();
//往后挪一位 下一次就能访问下一位元素
cursor = i + 1;
//将需要访问的元素返回
return (E) elementData[lastRet = i];
}
//移除当前访问到的最后一位元素
public void remove() {
//入参检测
if (lastRet < 0)
throw new IllegalStateException();
//判断一下该列表是否被其他线程改过(在迭代过程中) 修改过则抛异常
checkForComodification();
try {
//移除当前访问到的最后一位元素
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//快速遍历列表
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer
关注 郭霖
微信扫一扫关注公众号