ArrayList源码学习
ArrayList是什么
集合==》List接口 ==》List中常用的两个实现类ArrayList、LinkedList;
ArrayList是集合的一种实现,实现了接口List,List接口继承了Collection接口。Collection是所有集合类的父类。ArrayList就是一个以动态数组形式实现的集合类,从数据结构上来讲,是数组实现的线性表(即顺序表)。所以优势是随机访问元素,但是在list中间插入和移动元素时较慢。特别是插入效率。
ArrayList的继承关系
ArrayList继承AbstractList抽象父类,实现了List接口(规定了List的操作规范)、RandomAccess(可随机访问)、Cloneable(可拷贝)、Serializable(可序列化)。
数据结构
ArrayList的数据结构是:
底层的数据结构就是数组,数组元素类型为Object类型,即可以存放所有类型数据。
我们对ArrayList类的实例的所有的操作底层都是基于数组的。
特点
- 容量不固定,想放多少放多少(当size大于capacity的时候,自动进行1.5倍扩容;添加的时候,容量加一)
- 有序的(元素输出顺序与输入顺序一致)
- 元素可以为 null
- 元素不唯一(可重复)
- 不同步,与之对应的是,vector是同步的
- 可以通过索引直接操作元素,而set不行
ArrayList的线程安全性【未完】
为什么说ArrayList是线程不安全的?
ArrayList 线程安全问题
对ArrayList进行添加元素的操作的时候是分两个步骤进行的,即第一步先在object[size]的位置上存放需要添加的元素;第二步将size的值增加1。由于这个过程在多线程的环境下是不能保证具有原子性的,因此ArrayList在多线程的环境下是线程不安全的。
具体举例说明:在单线程运行的情况下,如果Size = 0,添加一个元素后,此元素在位置 0,而且Size=1;而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增 加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而Size却等于 2。这就是“线程不安全”了。
如果非要在多线程的环境下使用ArrayList,就需要保证它的线程安全性,通常有两种解决办法:第一,使用synchronized关键字;第二,可以用Collections类中的静态方法synchronizedList();对ArrayList进行调用即可。
主要成员变量
- capacity:数组容量(长度),默认是10
- size:当前元素个数
由于要维护数组的size,即动态数组的实现就是扩容时将原数组的元素复制到更大数组中。
所以在此之前补充下复制数组元素函数,java中的四种拷贝方式分别是: 循环赋值,System.arraycopy()
, Arrays.copyOf()
(或者Arrays.copyOfRange)和clone()
方法。具体四种方式分析: 学习笔记之 —— java数组的四种拷贝方式
本篇笔记主要涉及System.arraycopy() 和Arrays.copyOf(),其中
System.arraycopy
需要考虑自身复制重叠的情况,根据from>to,来判读是向前拷贝还是向后拷贝。
Arrays.copyOf()
源码可见其调用的是 System.arraycopy()
;
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 版本号
private static final long serialVersionUID = 8683452581122892189L;
// 缺省容量,当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组的容量大小,为10。
private static final int DEFAULT_CAPACITY = 10;
// 空对象数组,当ArrayList的构造方法中显示指出ArrayList的数组长度为0时,类内部将EMPTY_ELEMENTDATA 这个空对象数组赋给elemetData数组。
private static final Object[] EMPTY_ELEMENTDATA = {};
// 缺省空对象数组,当ArrayList的构造方法中没有显示指出ArrayList的数组长度时,类内部使用默认缺省时对象数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 元素数组,ArrayList的底层数据结构,只是一个对象数组,用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候此字段是不会被序列化的。
transient Object[] elementData;
// 实际ArrayList中存放的元素的个数,默认时为0个元素。
private int size;
// ArrayList中的对象数组的最大数组容量为Integer.MAX_VALUE – 8。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
方法
1.构造方法
- 初始为空数组public ArrayList()
- 根据指定容量,创建个对象数组public ArrayList(int initialCapacity)
- 直接创建和指定集合一样内容的 ArrayList public ArrayList(Collection<? extends E> c)
具体分为三种构造方法:
1)无参构造方法
/**
* Constructs an empty list with an initial capacity of ten.构造一个初始容量为10的空列表。
*/
public ArrayList() {
//调用父类中的无参构造方法,父类中的是个空的构造方法
super();
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为默认容量大小为10的空的Object[]类型元素数据
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2)有参构造函数
/**
* 构造一个具有指定初始容量的空列表。
* @param initialCapacity 为列表的初始容量
* @throws IllegalArgumentException 如果指定的初始容量为负
*/
public ArrayList(int initialCapacity) {
super();
if (initialCapacity > 0) { //判断自定义大小的容量情况,> 0正常创建,容量大小为initialCapacity
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //== 0直接赋值EMPT_ELEMENTDATA
this.elementData = EMPTY_ELEMENTDATA;
} else { //< 0抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
3)有参构造方法(不常用):指定 collection 列表
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); //c转换为数组,并赋值给elementData元素数组
if ((size = elementData.length) != 0) { //c中是否有内容
if (elementData.getClass() != Object[].class) // 每个集合的toarray()的实现方法不一样,所以需要判断是否为Object[].class类型,
elementData = Arrays.copyOf(elementData, size, Object[].class); //把elementData改为Object[].class类型
} else { // c中若无内容,用空数组替换。
this.elementData = EMPTY_ELEMENTDATA;
}
}
}
扩容
简单来说,就是将数组扩容到渴望的数组大小minCapacity
(所需的最小容量)
1、首先从ensureCapacityInternal(int minCapacity)
开始,确保内部容量:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
分别用到calculateCapacity(elementData, minCapacity)
和ensureExplicitCapacity(int minCapacity)
方法,分别展开:
2、调用calculateCapacity(elementData, minCapacity)
,即:
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //若数组是无参初始化状态
return Math.max(DEFAULT_CAPACITY, minCapacity); //从10开始扩容,返回理想最小值和默认值中最大的为新的理想最小值
}
return minCapacity;
}
3、调用ensureExplicitCapacity(int minCapacity)
,即确保准确的容量:
private void ensureExplicitCapacity(int minCapacity) {
modCount++; //已从结构上修改此列表的次数++。结构修改是那些改变列表大小的修改,或者以这样一种方式扰乱列表,使得正在进行的迭代可能产生不正确的结果。
// overflow-conscious code 溢出的代码
if (minCapacity - elementData.length > 0)//检测扩容容量minCapacity要大于elementData.length,若大于则需要扩容
grow(minCapacity);
}
4、ensureExplicitCapacity
中调用了grow(int minCapacity)
:开始扩容(最关键的部分):
- 每次从 1.5*oldCapacity 扩容;
- 若minCapacity大于1.5*oldCapacity,则选minCapacity;
- 若扩容超过
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
(MAX_ARRAY_SIZE
:要分配的数组的最大大小。一些vm在一个数组中保留一些标题词。尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过VM限制),
则调用hugeCapacity(int minCapacity)
选择,最大限制的容量minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
- 最后elementData = Arrays.copyOf(elementData, newCapacity);复制数组到扩容后的新数组。
/**
* 增加容量,以确保它至少可以容纳最小容量参数指定的元素数量。
*
* @param minCapacity 所需的最小容量
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //扩容前elementData的大小
//扩容1.5倍是因为:(oldCapacity + (oldCapacity >> 1))约是oldCapacity 的1.5倍。>>后约为原先的0.5倍。
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; //这句话就是适应elementData空数组的时候,length=0,那么oldCapacity=0,newCapacity=0,所以这个判断成立,在这里就是真正的初始化elementData的大小了,前面的工作都是准备工作。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//minCapacity通常接近于size,所以这是一个优势:
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;
}
2.增 add()
add()方法
1)boolean add(E); //默认直接在末尾添加元素
/**
* 将指定的元素追加到此列表的末尾。
* @param e 元素添加到此列表中
* @return boolean (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确定内部容量是否够了,size是数组中数据的个数,添加一个元素size+1。
elementData[size++] = e; //列表的末尾加上e
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
2)void add(int index, E element) //在列表的指定位置插入元素
/**
* 将指定的元素追加到此列表指定位置
* @param index
* @param element
*/
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++;
}
3)boolean addAll(Collection<? extends E> c) //将集合c的所有元素添加到列表中
/**
* 往指定列表插入集合c的所有元素
* @param c
* @return
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray(); //c转换为数组,并赋值给object型数组a
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0; //集合c中无数据,add失败
}
4)boolean addAll(int index, Collection<? extends E> c) //从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。
/**
* 从指定位置开始,将指定集合中的所有元素插入到此列表中。
* 将当前位于该位置的元素(如果有的话)和任何后续元素向右移动(增加它们的索引)。新元素将按照指定集合的迭代器返回它们的顺序出现在列表中。
*
* @param index 从指定集合中插入第一个元素的索引
* @param c collection,包含要添加到此列表的元素
* @return <tt>true</tt>如果这个列表由于调用而改变
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException 如果指定的集合为空
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index); //判断index是否在(0,size)之间
Object[] a = c.toArray(); //c转换为数组,并赋值给object型数组a
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
//ArrayList的大小(它包含的元素的数量)。
int numMoved = size - index; //需要后移的元素数量
if (numMoved > 0) //size可能会等于index
System.arraycopy(elementData, index, elementData, index + numNew, numMoved); //将插入新元素导致的原列表需要改动的元素后移
System.arraycopy(a, 0, elementData, index, numNew); //复制新数组到原数组中
size += numNew; //size加上新集合的大小
return numNew != 0;
}
/**
* 由add和addAll使用的rangeCheck的一个版本。
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/*
* 构造一个IndexOutOfBoundsException详细信息。在错误处理代码的许多可能重构中,这种“大纲”在服务器和客户端vm上执行得最好。
*/
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
3.删remove()
remove()方法
1)remove(int index): 通过删除指定位置上的元素
/**
* 移除列表中指定位置的元素。
* 将任何后续元素向左移动(从它们的索引中减去1)。
*
* @param index 要删除的元素的索引
* @return 从列表中删除的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
rangeCheck(index); // 检查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; // 清除,将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它
return oldValue;// 返回删除的元素
}
- remove(Object o):这个方法可以看出来,arrayList是可以存放null值的
/**
* 通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemove(index),使用这个方法来删除该元素
* arrayList可以存储null值
*
* @param o 元素将从此列表中删除(如果存在)
* @return <tt>true</tt> 如果此列表包含指定的元素
*/
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++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
- removeAll(Collection<?> c)
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);//如果C为空,抛出空指针异常
return batchRemove(c, false); // 批量删除
}
- clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉,所以叫clear
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
- batchRemove(Collection<?> c, boolean complement):用于两个方法
- removeAll():它只清除指定集合中的元素,当complement为false
- retainAll():用来测试两个集合是否有交集,当complement为true
/**
*仅保留此列表中包含在指定集合中的元素。换句话说,从这个列表中删除指定集合中不包含的所有元素。
*/
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
/**
*这里先假设elementData(ArryList内部维护的一个Object[]数组)和集合c的交集是A:
* 当 complement == true 时,elementData最终只会存储A
* 当 complement == false 时,elementData最终删除A。
*
* 在对elementData的元素进行筛选的时候,这里使用了r、w两个游标, 从而避免从新开辟一个新的数组进行存储。
*/
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData; // 将原集合元素数据放入对象数组elementData中
int r = 0, w = 0; // r 控制循环,w 记录有多少个交集
boolean modified = false; //modified 是否已修改
try {
for (; r < size; r++)
//当调用removeAll(Collection<?> c)时,complement == false,此时elementData数组中存储的是去掉A的部分
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r]; // 有的话,就给集合A
} finally {
// 即使c.contains()抛出,也要保持与AbstractCollection的行为兼容性。
if (r != size) { //出现异常会导致 r !=size , 则将出现异常处后面的数据全部复制覆盖到数组里。
System.arraycopy(elementData, r, elementData, w,size - r);// 将剩下的元素都赋值给集合A
w += size - r;
}
if (w != size) {
// 这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是null
// retainAll():没有一个交集返回true,有交集但不全交也返回true,有两个集合相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。
// 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;
}
// r == size: 注意到只有 elementData和都为空或者c的元素都不在elementData中的情况下,才会出现 w == size,因为都为空或者没有相同元素,所以不存在删除的情况。
总结:remove函数用户移除指定下标的元素,此时会指定下标到数组末尾的元素向前移动一个单位。,并且会把数组最后一个元素设置为null,这样是为了方便之后将整个数组不被使用时,会被GC,可以作为小的技巧使用。
4.改 set()方法
public E set(int index, E element) {
rangeCheck(index); // 检验索引是否合法
E oldValue = elementData(index); // 旧值
elementData[index] = element; // 赋新值
return oldValue; // 返回旧值
}
说明:设定指定下标索引的元素值
5.查 indexOf()方法、get()方法
/**
*返回此列表中指定元素的第一个匹配项的索引,如果此列表不包含该元素,则返回-1。
*/
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; // 没有找到,返回空
}
说明:从头开始查找与指定元素相等的元素,注意,是可以查找null元素的,意味着ArrayList中可以存放null元素的。与此函数对应的lastIndexOf,表示从尾部开始查找。
public E get(int index) {
rangeCheck(index); //检验索引是否合法
return elementData(index);
}
说明:get函数会检查索引值是否合法(只检查是否大于size,而没有检查是否小于0),值得注意的是,在get函数中存在element函数,element函数用于返回具体的元素,具体函数如下:
E elementData(int index) {
return (E) elementData[index];
}
说明:返回的值经过了向下转型(Object -> E),这些是对我们应用程序屏蔽的小细节
6.其他方法
size() : 获取集合长度,通过定义在ArrayList中的私有变量size得到
isEmpty():是否为空,通过定义在ArrayList中的私有变量size得到
contains(Object o):是否包含某个元素,通过遍历底层数组elementData,通过equals或==进行判断
clear():集合清空,通过遍历底层数组elementData,设置为null
转化成数组
// 返回ArrayList的Object数组
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
// 返回ArrayList元素组成的数组
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
// 若数组a的大小 < ArrayList的元素个数;
// 则新建一个T[]数组,数组大小是“ArrayList的元素个数”,并将“ArrayList”全部拷贝到新数组中
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// 若数组a的大小 >= ArrayList的元素个数;
// 则将ArrayList的全部元素都拷贝到数组a中
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}