参考
https://blog.csdn.net/u010250240/article/details/89762912
https://www.jianshu.com/p/ccbe93b5ec76
之前写过一篇读源码的博客,第一次尝试着去阅读ArrayList的源码,算是在漫长的CRUD生活中添加了一缕别样的色彩。
当我自己再去读这篇博客的时候,您猜猜怎么着?哎,我当时就忍不住笑了出来,那更像是一篇充满了错别字的阅读笔记,而且是顺序阅读代码,对整体实现思路的总结没有,还有不少错误的描述,但是考虑到我第一次读源码,尝试从未设想的道路,任何一样工作,在刚展开的时候一定是会有困难的,情况的改善往往出现在 “再坚持一下”的努力中。不管它多么不好,都是一次勇敢的尝试,它为我打开了新世界的大门,对我来说,它的意义是非凡的,所以,尽管它不好,我还是决定留下它。
还是一句话,学习,态度要端正,这一次我决定重新整理,仔细阅读别的优质博客,加上自己之前对源码的些许记忆,重新整理,为这个问题交一份新的答卷。
在开始整理之前,我们需要先思考一个问题:我们为什么要去读源码?
一个软件,一个工具,其必然是能为人们提供一定的辅助作用,我们要做的是了解工具的运行原理,了解工具的适用场景,并对其局限性有一定了解。说人话就是:知道这个东西能做到什么,怎么做到的,以及做不到什么。即,工具的能力和局限性
ArrayList是基于数组实现的,为什么我们选用ArrayList存储数据而非Object数组?二维数组可以实现类似Hash'Map的效果,也可以存key-value键值对,为什么我们要用HashMap而不是自己弄一个二维数组呢?
带着几个问题看下去吧:ArrayList能为我们做什么?ArrayList的特性是什么?至于在问题中带出来的细节,可以具体查阅资料再去处理,我先前的博客就是太过于扣部分细节丢失了整体了解,显得混乱
ArrayList顶部注释:
List接口的大小可变数组的实现。实现了所有可选列表操作,并允许包括null在内的所有元素。除了实现List接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。(此类大致上等同于Vector类,除了此类是不同步的。)
size、isEmpty、get、set、iterator和listIterator操作都以固定时间运行。add操作以分摊的固定时间运行,也就是说,添加n个元素需要O(n)时间。其他所有操作都以线性时间运行(大体上讲)。与用于LinkedList实现的常数因子相比,此实现的常数因子较低。
每个ArrayList实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。
在添加大量元素前,应用程序可以使用ensureCapacity操作来增加ArrayList实例的容量。这可以减少递增式再分配的数量。
注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:
List list = Collections.synchronizedList(new ArrayList(…));
此类的iterator和listIterator方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的remove或add方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测bug。
此类是Java Collections Framework的成员。
先来补充一个小知识,时间复杂度,假如一行a = b +c;它的复杂度就是O(1),同理,循环n次的就是O(n),两个n次循环嵌套的就是O(n平方),懒得研究平方二怎么打,就这样吧,代码的复杂度越低,性能越优秀。
通过上面的注释,我们对ArrayList有一个大概的了解
1 基于数组实现,且本质上相当于一个能自动扩容的可变长度数组
2 线程不安全,多线程环境使用它需要处理
3 允许存入null,允许存入重复的值(废话,数组当然可以存重复数据)
4 遍历时,使用for循环的遍历效率将高于迭代器遍历,且使用迭代器遍历时,只能使用迭代器方法remove或者add元素
5 基于数组实现,那么有数组的特性,查询快(因为有下标),而增删相对就会较慢
6 属于Collections接口下大家族中的一员,这意味着,很多Collections接口的方法它时可以用的,且ArrayList可以和Collections接口下的其它实现类互相转换,如Set,也可以和数组相互转换
1 继承与实现
1.1 实现Serializable接口,说明这个类可以被序列化为二进制数据传输,然后可以反序列化成ArrayList
1.2 实现Cloneable 说明该类的属性时可以被克隆的,克隆的概念类似属性赋值,分深度克隆和浅度克隆两种,这里不深究
1.3 List接口,这个不用多说了
1.4 继承AbstractList抽象类,抽象类与接口的区别,就是抽象类中时可以有非抽象方法的,那对于List的通用方法就可以实现在抽象类中,而子类直接用线程的就好
为什么继承了抽象List,还要实现List接口呢?我个人理解是便于反射处理,实现List接口,反射拿到的就是List
2 属性及意义
/**
* Default initial capacity.
* 默认容量,长度为10的数组
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* Shared empty array instance used for empty instances.
* 当指定容量为0的list被创建时,返回的就是这个静态常量空数组
* 所有指定容量为0的list都用这个数组表示,避免浪费
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
* 这也是个静态常量空数组,但与上面的空数组不同,这个空数组是用来
* 标识容量为0,但是会存元素的list实例的,往下看
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
* 缓冲数组,list实例存数据的地方,它的长度就是ArrayList的容量
* 对于有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA的ArrayList
* 即调用new ArrayList()创建的,当第一个元素添加时,数组会被扩容至默认长度(10)
* 这个数组是非私有的,方便嵌套数据,如ArrayList<ArrayList>类型的数据
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* The size of the ArrayList (the number of elements it contains).
*
* list包含的元素个数
*/
private int size;
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
* 能为数组分配的最大长度,部分虚拟机需要8位空间存储头信息
* 所以-8,否则可能造成内存溢出
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
3 构造方法
//initialCapacity大小即ArrayList指定容量,会先对该值进行校验
//大于0,则直接创建一个该长度的数组
//等于0 则将EMPTY_ELEMENTDATA(空List通用数组) 赋值给elementData
//小于0则属于非法参数
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);
}
}
//无参构造,只做了一件事,将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//接收一个Collection类型参数,可以是set,也可以是list
//所以set怎么转list?没错,直接新建,作为参数传进去
public ArrayList(Collection<? extends E> c) {
//治理调用的是c的toArray()方法,每个Collection的子类具体实现可能不同
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//toArray()方法可能不会正确的返回Object数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
//若toArray返回Object[]则直接赋值,若不是Object[]则转成Object[]赋值,否则赋空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
//Arrays.copyOf()方法,其作用,就是将上面的enementData转换为Object数组
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
小结:构造方法始终都是在操作elementData属性,(先挖个坑,除了该属性未设置私有便于嵌套,该数组为什么会设置成trainsent属性避免序列化?这样序列化ArratList不会丢失数据么?),elementData是真正存储数据的地方,其长度也就是ArrayList的容量,ArrayList是有动态扩容功能的,jdk的开发者一定会考虑性能问题和扩容,其实在这里不难联想 线程池 连接池 HashMap,凡是作为容器存储数据就一定会考虑尽量小但是满足需求的初始化容量,且在容量不满足存储任务时触发扩容机制
4 常见方法
4.1 get
//获取指定下标的元素,本质就是数组中指定下标的元素
//注意数组下标从0开始,ArrayList也是如此
//获取之前先进行越界检查,若所传下标超过size,则抛出越界异常
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
4.2 add
//先前提到过,若ArrayList是调用无参构造创建的,则会赋值一个空数组
//并在添加第一个新元素时候,确认容量
// 1 如果是第一个元素,此时size未初始化,未初始化的int属性会默认初始成0
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//将元素塞进数组内下标为size的空间(首次size=0),size自增1
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//将10 或者是当前的实际元素个数size +1和当前数组长度比较
//数组容量小于该数值说明需要扩容
private void ensureExplicitCapacity(int minCapacity) {
//迭代器需要用到这个属性值,初始化设置为0
modCount++;
// overflow-conscious code
//该方法可能会导致内存溢出
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//判断是不是新建的空List,如果是,则返回size + 1 和10中较大的一个
//注意,这里的size不是数组长度,而是真实的元素个数
//如果不是,则直接返回10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//只有新的空List才能触发这个if条件
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
这里回顾上面的说明
但我们新建一个ArrayList list = new ArrayList()的时候,list内部的elmentData数组是被赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个常量空数组的
也是个标记数组,这个标记的作用就是判断list实例是否是一个空list,如果等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA说明就不是
那么在新增第一个元素的时候,此时size未初始化,为0,通过一个比较算法calculateCapacity算出了10这个数字
在ensureExplicitCapacity方法中,之前计算得到的10,和当前elementData长度对比,若大于当前长度,则执行扩容,确认elementData的真正长度
Tips:这时候,设计size这个字段的价值就体现出来了,elementData的长度是ArrayList的容量,而size,是其中元素的个数(int类型自动初始化为0)
每次新增元素的时候,size+1(真实的元素个数) 先和默认长度10做对比,取较大值,再取这个返回值和elementData当前长度做对比,确定是否扩容
看一下扩容的方法 grow()
//接上面,拿到minCapacity之后
//第一次向无参构造创建的list添加元素,length是0,做扩容
//通过新长度 = 旧长度 + 旧长度带符号右移两位(除以2)
//然后新长度和minCapacity对比,若新长度小于minCapacity,则直接赋值等于minCapacity
//否则,调用hugeCapacity,如果minCapacity超出int范围,就会变为负数,此时内存肯定溢出
//否则做比较, Integer.MAX_VALUE ,MAX_ARRAY_SIZE中选一个作为elementData长度
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
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;
}
这里分析的一直是未指定初始长度的,那如果是指定了初始长度的呢?
如果指定了初始长度是7,那么返回的就是new Object[7],此时不满足elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA条件,对比的方法返回的就是7,7-7=0而不是>0,所以不会触发扩容
add方法还有一个多参数的版本
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*将指定元素插入到列表中的指定位置。将当前位于该位置的元素(如果有的话)和随后的任何元素向右移动(将一个元素添加到它们的索引中)。
* @param index index at which the specified element is to be inserted
* 要插入指定元素的索引(即将插入元素的位置)
* @param element element to be inserted 即将插入的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
//越界检查
rangeCheckForAdd(index);
//确认list容量,如果不够,容量加1。注意:只加1,保证资源不被浪费
ensureCapacityInternal(size + 1); // Increments modCount!!
// 对数组进行复制处理,目的就是空出index的位置插入element,并将index后的元素位移一个位置
//在插入元素之前,要先将index之后的元素都往后移一位
//arraycopy(原数组,源数组中的起始位置,目标数组,目标数据中的起始位置,要复制的数组元素的数量)
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//将指定的index位置赋值为element
elementData[index] = element;
//实际容量+1
size++;
}
/**
* A version of rangeCheck used by add and addAll.
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)//插入的位置不能大于size 和小于0,如果是就报越界异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
4.3 remove
public E remove(int index) {
//越界检查
rangeCheck(index);
//modCount自增,如果迭代器遍历检测到这个不为0就会报错
modCount++;
//需要移除的值
E oldValue = elementData(index);
//需要左移的元素个数,因为index是从0开始的,需要多减去1
int numMoved = size - index - 1;
if (numMoved > 0)
//将原数组index+1和之后的元素复制到index及之后
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//减少了一个元素,长度减一,将最后一个元素置null,GC回收
elementData[--size] = null; // clear to let GC do its work
//返回被删除的元素
return oldValue;
}
到这里我们可以发现,add和remove这样会改变list元素个数的方法都会导致越界检查,我们试着联想一下,数据库中的事务,如果只是查询(不要说select for update),你事务提交不提交没关系,但是增删改数据提交事务才生效,事务何尝不是一种检查呢?不过事务的作用不止检查,还要更多
4.4 retainAll
//核心思路就是将elementData赋值给一个新的引用,然后对比
//将相同的元素都置于新引用的前w个位置,移除其余的元素,只保留双集合中相同的元素
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
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 {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
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;
}
retainAll会保留两个集合中的相同元素,删除不同元素,另一个removeAll的方法只是将complement取反,就会去除相同元素而保留不同元素,这一段在原先的旧博客中我读过,就补再读一遍了。
4.5 clear
/挨个置为null让GC回收
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
这里再提一下,为什么enementData被声明了不序列化,这个数组中是可能有很多null的,真正的数据只占其中一部分,如果全都序列化,可能不少null会被序列化
而ArrayList中的writeObject方法保证了正确的内容可以被正确的序列化
关于序列化具体看这里
总结:
若ArrayList为无参构造初始化,当第一个元素被添加时,容量会被扩充为10,若不添加元素,则还是0(不适用list,自然不浪费内存)
指定大小的list,添加元素时不会触发扩容
ArrayList存储的时Object类型,允许null元素
由于未采取任何控制线程安全的手段,ArrayList是线程不安全的
可以通过list.subList(from, to).clear();清除指定部分内容
对ArrayList来说,for循环遍历的效果要强于迭代器遍历(这里面牵扯到遍历的原理)