这是我在学习Java集合框架时对 ArrayList 的源码级解析,我将许多常用的方法都做出了解释,还有许多更为底层的地方没有涉及到,等到以后有机会我再补全。如果这篇文章对您有帮助的话,希望能给作者一个赞哦!!!
文章目录
类图
- 实现了
RandomAccess
接口,可以随机访问- 实现了
Cloneable
接口,可以克隆- 实现了
Serializable
接口,可以序列化、反序列化- 实现了
List
接口,是List
的实现类之一- 实现了
Collection
接口,是Java Collections Framework
成员之一- 实现了
Iterable
接口,可以使用for-each
迭代
ArrayList 底层是用数组实现的存储。
特点:可自动扩容,查询效率高,增删效率低,线程不安全。
ArrayList 类注释释义:
- ArrayList是实现
List
接口的可自动扩容的数组。实现了所有的List
操作,允许所有的元素,包括null
值。- ArrayList大致和Vector相同,除了ArrayList是非同步的。
size
isEmpty
get
set
iterator
和listIterator
方法时间复杂度是O(1)
,常量时间。其他方法是O(n)
,线性时间。- 每一个ArrayList实例都有一个
capacity
(容量)。capacity
是用于存储列表中元素的数组的大小。capacity
至少和列表的大小一样大。- 如果多个线程同时访问ArrayList的实例,并且至少一个线程会修改,必须在外部保证ArrayList的同步。修改包括添加删除扩容等操作,仅仅设置值不包括。这种场景可以用其他的一些封装好的同步的
list
。如果不存在这样的Object
,ArrayList应该用Collections.synchronizedList
包装起来最好在创建的时候就包装起来,来保证同步访问。iterator()
和listIterator(int)
方法是fail-fast
的,如果在迭代器创建之后,列表进行结构化修改,迭代器会抛出ConcurrentModificationException
。- 面对并发修改,迭代器快速失败、清理,而不是在未知的时间不确定的情况下冒险。请注意,快速失败行为不能被保证。通常来讲,不能同步进行的并发修改几乎不可能做任何保证。因此,写依赖这个异常的程序的代码是错误的,快速失败行为应该仅仅用于防止
bug
。
1. ArrayList 的属性
ArrayList 中共有7个属性:
属性解析:
// 序列化版本UID
private static final long serialVersionUID = 8683452581122892189L;
/**
* 默认的初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于零容量列表的共享空数组实例
* new ArrayList(0);
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用于提供默认大小(10)列表的共享空数组实例
* new ArrayList();
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存储ArrayList元素的数组缓冲区
* ArrayList的容量,是数组的长度
*
* non-private to simplify nested class access
* 非私有以简化嵌套类访问
*/
transient Object[] elementData;
/**
* ArrayList中元素的数量
*/
private int size;
transient 关键字
1)一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient 关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被 transient 关键字修饰的。变量如果是用户自定义类变量,则该类需要实现 Serializable 接口。
3)被 transient 关键字修饰的变量不再能被序列化,一个静态变量不管是否被 transient 修饰,均不能被序列化。
ArrayList 底层其实就是⼀个数组,ArrayList 中有扩容这么⼀个概念,正因为它扩容,所以它能够实现“动态”增长
2. 构造方法
ArrayList 中有一个无参构造器,两个有参构造器:
2.1 public ArrayList(int initialCapacity)
带一个初始容量参数的构造方法
/**
* 构造一个容量为initialCapacity的空列表
* @param initialCapacity 人为传入的列表的初始化容量
* @throws IllegalArgumentException
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//传入参数>0,则以此容量构造一个空Object数组,赋给底层数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//传入参数=0,则将属性中的0容量空数组EMPTY_ELEMENTDATA赋给底层数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
//传入参数<0,参容量非法,抛出异常
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
2.2 public ArrayList()
无参构造方法
/**
* 构造一个容量为默认值10的空列表
*/
public ArrayList() {
//使用空参构造器,则将属性中的默认的10容量空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给底层数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2.3 public ArrayList(Collection<? extends E> c)
带一个集合参数的构造方法
/**
* 构造一个列表,该列表包含了传入的集合中的所有元素,元素的添加顺序和它们被 传入的集合的迭代器 返回的 * 顺序一致
* @param c 元素将被放进新列表的集合
* @throws NullPointerException
*/
public ArrayList(Collection<? extends E> c) {
//将集合中的对象传进一个数组中
Object[] a = c.toArray();
if ((size = a.length) != 0) {
//将数组大小赋给size(新建ArrayList的实际大小),如果不等于0
if (c.getClass() == ArrayList.class) {
//如果传入集合的类型是ArrayList,直接将集合的对象数组赋给底层数组,这里是为了防止 c.toArray 方法不正确的执行导致没有返回Object[]
elementData = a;
} else {
//否则将集合的对象数组中的元素逐个拷贝到大小为size的Object数组中,再赋给底层数组
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
//如果传入的集合是空的,则将属性中的0容量空数组EMPTY_ELEMENTDATA赋给底层数组
elementData = EMPTY_ELEMENTDATA;
}
}
什么情况下
c.toArray()
会不返回Object[]
呢?
java.util.ArrayList.toArray()
方法会返回Object[]
没有问题。而java.util.Arrays
的私有内部类 ArrayList 的toArray()
方法可能不返回Object[]
。因为java.util.Arrays
的内部 ArrayList 的toArray()
方法,是构造方法接收什么类型的数组,就返回什么类型的数组。
3. 添加方法 add()
总览 add() 方法:
3.1 public boolean add(E e)
在列表最后添加指定元素
步骤:
- 检查是否需要扩容
- 插入元素
/**
* 将指定元素添加到列表尾部
* @param e 需要添加到列表尾部的元素
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!——增加modCount
elementData[size++] = e;
return true;
}
在父类
AbstractList
上,定义了modCount
属性,用于记录数组修改的次数。
其中,ensureCapacityInternal(size + 1);
有如下作用:
- 确认 list 容量,尝试将容量 + 1,看看有无必要
- 添加元素
接下来看看这个方法的工作机理:
当添加第11个元素时,minCapacity 为11,底层数组长度为10,那么就需要扩容了
接下来再来看看grow()
方法的实现:
- 通常情况新容量是原来容量的1.5倍
- 如果原容量的1.5倍比
minCapacity
小,那么就扩容到minCapacity
- 特殊情况扩容到
Integer.MAX_VALUE
进入copyOf()
方法瞧瞧~
到此为止,add(E e)
的实现流程如下:
首先去检查一下数组的容量是否足够
- 足够:直接添加新元素
- 不足够:扩容
- 将列表容量扩容到原来的 1.5 倍
- 第⼀次扩容后,如果容量还是小于minCapacity,就将容量扩充为 minCapacity。
3.2 add(int index, E element)
在指定位置添加指定元素
步骤:
- 检查角标
- 空间检查,如果有需要进行扩容
- 插入元素
/**
* 在指定位置添加指定元素
* 如果指定位置已经有元素,就将该元素和随后的元素移动到右面一位
*
* @param index 待插入元素的下标
* @param element 待插入的元素
* @throws 可能抛出 IndexOutOfBoundsException
*/
public void add(int index, E element) {
rangeCheckForAdd(index);//检查角标是否越界
// 增加 modCount !!
ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1, size - index);//调用arraycopy()进行插入
elementData[index] = element;
size++;
}
看到 arraycopy()
,我们可以发现:该方法是由C/C++ 来编写的,并不是由 Java 实现:
4. 移除方法 remove()
4.1 public E remove(int index)
移除指定下标元素方法
步骤:
- 检查角标
- 删除元素
- 计算出需要移动的个数,并移动
- 设置为null,让Gc回收
详解:
/**
* 移除列表中指定下标位置的元素
* 将所有的后续元素,向左移动
*
* @param 要移除的指定下标
* @return 返回被移除的元素
* @throws 下标越界会抛出IndexOutOfBoundsException
*/
public E remove(int index) {
rangeCheck(index);//检查下标合法性
modCount++;//数组修改的次数+1
E oldValue = elementData(index);
//将所删除元素的右边元素逐个向左移动
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 将引用的最后一个置空,让GC回收
elementData[--size] = null;
return oldValue;
}
4.2 public boolean remove(Object o)
移除指定元素方法
详解:
/**
* 移除第一个在列表中出现的指定元素
* 如果存在,移除返回true
* 否则,返回false
*
* @param o 指定元素
*/
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;
}
移除方法名字、参数的个数都一样,使用的时候要注意。
4.3 private void fastRemove(int index)
私有移除方法
详解:
/*
* 私有的 移除 方法 跳过边界检查且不返回移除的元素
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
//将引用置空,让GC回收
elementData[--size] = null;
}
5. 查找方法 get()
5.1 public E get(int index)
查找指定位置的元素
详解:
/**
* 返回指定位置的元素
*
* @param index 指定元素的位置
* @throws index越界会抛出IndexOutOfBoundsException
*/
public E get(int index) {
rangeCheck(index);//检查角标
return elementData(index);//返回具体元素
}
检查角标:
// 检查⻆标
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
返回元素:
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
5.2 public int indexOf(Object o)
查找指定元素的所在位置
详解:
/**
* 返回指定元素第一次出现的下标
* 如果不存在该元素,返回 -1
* 如果 o == null 会特殊处理
*/
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;
}
6. set() 方法
步骤:
- 检查角标
- 替代元素
- 返回旧值
7. 序列化方法 writeObject()
/**
* 将ArrayList实例的状态保存到一个流里面
*/
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// 按照顺序写入所有的元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
8. 反序列化方法 readObject()
/**
* 根据一个流(参数)重新生成一个ArrayList
*/
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt();
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
elementData
之所以用transient
修饰,是因为 JDK 不想将整个elementData
都序列化或者反序列化,而只是将size
和实际存储的元素序列化或反序列化,从而节省空间和时间。
9. subList() 方法
创建子数组列表
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);//检查传入参数的合理性
return new SubList(this, 0, fromIndex, toIndex);//返回创建好的子列表
}
其中的 subListRangeCheck()
方法:
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")");
}
再来看看 SubList 类的定义:
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
// 省略代码...
}
注意:
- SubList 的
set()
方法,是直接修改ArrayList中elementData
数组的,使用中应该注意 - SubList 是没有实现
Serializable
接口的,是不能序列化的
10. 迭代器实现
创建迭代器的方法:
接下来再来看看 Itr 这个类:
-
Itr 属性
// 下一个要返回的元素的下标 int cursor; // 最后一个要返回元素的下标 没有元素返回 -1 int lastRet = -1; // 期望的 modCount int expectedModCount = modCount;
-
Itr的 hasNext() 方法
public boolean hasNext() { return cursor != size; }
-
Itr的 next() 方法
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; return (E) elementData[lastRet = i]; } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
什么是并发修改异常?
当我们在对集合进行迭代操作的时候,如果同时对集合对象中的元素进行某些操作,则容易导致并发修改异常的产生。 这经常出现在多个线程同时对一个列表进行操作时。
-
Itr的 remove() 方法
public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; // 移除之后将modCount 重新赋值给 expectedModCount expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
11. 总结
-
ArrayList底层的数据结构是数组,在增删时候,需要数组的拷贝复制。
-
ArrayList可以自动扩容,不传初始容量或者初始容量是
0
,都会初始化一个空数组,但是如果添加元素,会自动进行扩容,所以,创建ArrayList的时候,给初始容量是必要的。【ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的⼀半,也就是变为原来的1.5倍】 -
Arrays.asList()
方法返回的是的Arrays
内部的ArrayList,用的时候需要注意 -
subList()
返回内部类,不能序列化,和ArrayList共用同一个数组 -
迭代删除要用迭代器的
remove
方法,或者可以用倒序的for
循环 -
ArrayList重写了序列化、反序列化方法,避免序列化、反序列化全部数组,浪费时间和空间
-
elementData
不使用private
修饰,可以简化内部类的访问 -
删除元素时不会减少容量,若希望减少容量则调用 trimToSize()
-
它不是线程安全的。它能存放null值。