没有一个冬天不可逾越,没有一个春天不会来临。
内容
1.ArrayList概述
ArrayList内部是使用动态数组实现的,也就是说,ArrayList封装了对内部数组的操作,比如向数组中添加、删除、插入新的元素、扩容等。
ArrayList类图:
ArrayList定义:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
从ArrayList的类图和定义,我们能获取到以下信息:
- ArrayList< E > : 说明ArrayList支持泛型,所以只能存放引用类型数据。
- extends AbstractList< E > : 继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作。
- implements List< E >:实现了List接口,该接口包含了有关列表的基本操作,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合。
- implements RandomAccess: 标识ArrayList支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
- implements Cloneable:表明ArrayList可以通过调用clone()来克隆他的对象。
- implements java.io.Serializable:表明ArrayList支持序列化。
2.ArrayList成员变量
ArrayList最主要的成员变量有以下两个:
- transient Object[] elementData : 用于存放元素的数组。
- private int size :数组实际元素的个数。
需要注意的是:elementData.length 为数组的容量。
如图:该数组的容量为10,size为9(实际只保存了9个元素)。
其余成员变量有:
- private static final int DEFAULT_CAPACITY = 10:默认容量。
- private static final Object[] EMPTY_ELEMENTDATA = {}:空数组。
- private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:默认空数组。
3.ArrayList构造方法和初始容量
ArrayList的构造方法有三个:
- ArrayList()
- ArrayList(int initialCapacity)
- ArrayList( Collection<? extends E> c)
3.1 ArrayList()
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//elementData 用于存放元素的数组
transient Object[] elementData;
//since 1.7
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
当我们创建一个空ArrayList集合时,其数组为空数组,即初始化容量为0。DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是 ArrayList的成员变量,elementData是真正用于存放元素的数组。
Tip:
- JDK1.6时,调用ArrayList无参构造会初始化一个容量为10的数组 。
- JDK1.7开始,调用ArrayList无参构造会直接赋值一个空数组,第一次调用add元素时,会扩容到10(扩容机制)。
// since 1.6
public ArrayList() {
this(10); // 调用ArrayList(int initialCapacity) 构造函数,初始化容量10
}
3.2 ArrayList(int initialCapacity)
initialCapacity用于指定ArrayList的初始数组容量。
private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { // 初始容量大于0
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) { //初始容量为0
this.elementData = EMPTY_ELEMENTDATA;
} else { //初始容量小于0
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
- 如果指定初始容量大于0,则构建指定长度的空数组并赋值给elementData。
- 如果指定初始容量等于0,则将已有的空数组赋值给elementData。
- 如果指定初始容量小于0,则将抛出IllegalArgumentException异常。
Tip:
指定初始容量等于0时,会将空数组EMPTY_ELEMENTDATA赋值给elementData,EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 都是空数组,但用途不同:
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA 用于空参构造赋值的默认空数组。
- EMPTY_ELEMENTDATA用于初始化容量为0或使用Collection集合构造函数时Collection中没有元素。
3.3 ArrayList( Collection<? extends E> c)
构造一个包含指定Collection集合元素的列表,这些元素可以是E或E的子类(E为ArrayList的泛型类型)并且是按照该collection的迭代器返回它们的顺序排列的。其容量和指定集合容量相同。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
该构造函数执行过程:
- 将Collection集合转为数组赋值给elementData。
- 若elementData长度不为0,设置size,并且若elementData不是Object类型,则转换为Object[]类型的数组。
- 若elementData长度为0,则将空数组EMPTY_ELEMENTDATA赋值给elementData。
4.ArrayList扩容机制(重点)
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
数组扩容的执行流程可分为以下步骤:
- 计算最小容量;
- 判断是否需要扩容;
- 计算新的容量;
- 考虑数组长度溢出;
- 数组扩容;
4.1 计算最小容量
calculateCapacity()方法用于计算最小容量。传入参数(minCapacity)是添加元素后的元素总数。(例如调用add,则minCapacity = size + 1 ; 调用 addAll ,则minCapacity = size + 被添加集合的size)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
- 若使用无参构造创建的ArrayList,在第一次扩容时,最小容量(minCapacity)的值为 默认容量10(DEFAULT_CAPACITY)和 元素总数的最大值。
- 否则最小容量(minCapacity)的值就是元素总数。
Tip:
elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 如果为true,那么说明:该集合是使用的默认空构造器初始化的,并且是第一次添加数据。
4.2 判断是否需要扩容
ensureExplicitCapacity()方法用于判断是否要进行扩容,若需要就进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
- modCount++ ,记录一次ArrayList结构变化。(用于快速失败[fail-fast])
- 若计算出的最小容量大于当前elementData数组的容量,则调用grow()进行扩容。
4.3 计算新的容量
确定扩容后,首先计算数组elementData的新容量。
private void grow(int minCapacity) {
// 计算新的容量
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 考虑数组长度溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
- newCapacity = oldCapacity + (oldCapacity >> 1);即新容量约为旧容量的1.5倍。
- 若 1.5倍扩容后依然比计算的最小容量(minCapacity)还小,那么数组的新容量(newCapacity )就设置成最小容量(minCapacity)。
4.4 考虑数组长度溢出
扩容后需要考虑数组长度溢出问题。
// 考虑数组长度溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
不管是计算minCapacity还是计算出的newCapacity 都存在数值溢出的问题。
例如:当ArrayList的容量和size都为Integer.MAX_VALUE时,再次添加一个元素,那么总元素数为Integer.MAX_VALUE + 1,minCapacity为int类型的值,所以必然溢出。newCapacity按原容量扩容1.5倍,同样存在溢出情况。
// MAX_ARRAY_SIZE : 建议最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
- 当minCapacity溢出时抛出OutOfMemoryError异常;
- minCapacity > MAX_ARRAY_SIZE 时 设置 newCapacity 等于Integer.MAX_VALUE。
- minCapacity <= MAX_ARRAY_SIZE 时 设置 newCapacity 等于 MAX_ARRAY_SIZE。
4.5 扩容
最后扩容,重新构建一个数组,然后将原来数组的元素拷贝到新数组中,新数组的长度为newCapacity。并修改原数组的引用指向这个新建数组,原数组自动抛弃(Java垃圾回收机制会自动回收)。
// 扩容
elementData = Arrays.copyOf(elementData, newCapacity);
Tip:
ArrayList所谓的可变长度,实际上在底层也只是新建一个更长的数组,然后拷贝原数组的元素到新数组,并将引用指向新数组而已。
4.6 扩容机制总结
总来说:添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此应该尽量减少扩容次数。
- 创建ArrayList对象时,可以指定大概容量。
- add之前调用ensureCapacity()方法指定大概容量。
5. 常用方法
签名 | 描述 | 复杂度 |
---|---|---|
E get(int index) | 获取指定位置的元素 | O(1) |
boolean add(E e) | 在集合末尾新增一个元素 | O(1) |
void add(int index, E element) | 在指定位置添加元素 | O(n) |
E remove(int index) | 删除指定位置的元素 | O(n) |
E remove(Object o) | 移除集合中第一次出现的指定元素 | O(n) |
int indexOf(Object o) | 查询指定元素首次的位置 | O(n) |
E set(int index, E element) | 设置指定位置的元素值 | O(1) |
boolean contains(Object o) | 是否包含某个元素 | O(n) |
int size() | 返回列表中的元素个数 | O(1) |
boolean isEmpty() | 列表是否为空 | O(1) |
void clear() | 从列表中移除所有元素 | O(n) |
6.ArrayList迭代器
ArrayList通过内部类实现Iterator接口来实例化迭代器类,通过Iterator我们可以实现对elementData中的元素迭代遍历。
iterator()方法返回一个Itr类的实例对象。
public Iterator<E> iterator() {
return new Itr();
}
Itr是ArrayList的一个内部类,实现了Iterator< E >接口。
private class Itr implements Iterator<E>
listIterator()方法返回一个ListItr类的实例对象。
public ListIterator<E> listIterator() {
return new ListItr(0);
}
ListItr类也是ArrayList的一个内部类,继承自Itr ,实现了ListIterator< E >接口。
private class ListItr extends Itr implements ListIterator<E>
- Iterator迭代器只能正向迭代。
- ListIterator迭代器可以正向迭代,也可以反向迭代。
- forEach(增强for)最终被编译器转换成对iterator.next()和iterator.hashNext()方法的调用,即forEach遍历方式等同于Iterator迭代器遍历。
public class ArrayListTest {
public static void main(String[] args) {
ArrayList list = new ArrayList(Arrays.asList(1, 2, 3, 4));
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
//list.remove(1);//Error : ConcurrentModificationException
}
System.out.println("\n------------");
ListIterator listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.print(listIterator.next() + " ");
}
System.out.println("\n------------");
while (listIterator.hasPrevious()) {
System.out.print(listIterator.previous() + " ");
}
}
}
输出:
1 2 3 4
------------
1 2 3 4
------------
4 3 2 1
注意:使用迭代器进行迭代的过程中,只能使用迭代器提供的add、remove进行结构修改,使用集合本身的add、remove等语句会引发ConcurrentModificationEeception 并发修改异常,这是因为迭代器是快速失败的(fail-fast)。
7.快速失败(fail-fast)与安全失败(fail-safe)
7.1 快速失败(fail-fast)
快速失败(fail-fast)是Java容器(Collection和Map都存在fail-fast)的一种错误检测机制。当方法检测到对象的并发修改,但不允许这种修改时就抛出ConcurrentModificationException 异常。
快速失败迭代器会尽最大努力抛出 ConcurrentModificationException,但不可能对是否出现不同步并发修改做出任何硬性保证。因此存在非同步的并发修改时,只是可能会抛出该异常。
快速失败(fail-fast)产生的场景:
- 单线程环境下,遍历集合时,集合结构被修改(使用迭代器修改则不会)。
- 多线程环境下,一个线程遍历集合时,另一个线程修改了集合结构。
快速失败(fail-fast)原理:modCount用于记录ArrayList结构修改的次数,modCount继承自AbstractList。在序列化之前或初始化迭代器时,会在内部设置expectedModCount变量等于外部的modCount变量,序列化后或每次进行迭代时,就会检测expectedModCount和modCount是否一致,当这两个变量不一致时,就抛出并发修改异常(ConcurrentModificationEeception )。
如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
Tip:
结构修改是指:添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
快速失败(fail-fast)解决方案:
- 单线程遍历集合时,使用迭代器提供的结构修改方法,如:add、remove。
- 多线程下,使用java.util.concurrent下的集合代替java.util下的集合类,如使用CopyOnWriteArrayList替代ArrayList。
7.2 安全失败(fail-safe)
安全失败(fail-safe):采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历(读写分离)。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception。
安全失败(fail-safe)缺点:
- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
Tip:
java.util.concurrent下的容器都是安全失败的(fail-safe)。
8.ArrayList序列化
ArrayList中的elementData被transient修饰,表明该成员变量不可被序列化。这是因为elementData中不一定全部被填充了元素,因此没有必要将elementData全部序列化。
transient Object[] elementData; // non-private to simplify nested class access
ArrayList通过实现 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
public class ArrayListTest {
public static void main(String[] args) throws Exception{
ArrayList list = new ArrayList(Arrays.asList(1, 2, 3, 4));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
oos.writeObject(list);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.txt"));
Object o = ois.readObject();
System.out.println(o);
}
}
- 在序列化过程中,ObjectOutputStream.writeObject() 和 ObjectInputStream .readObject() 方法会通过反射机制,尝试调用被序列化对象类里的 writeObject 和 readObject 方法,进行序列化和反序列化。
- 如果被序列化对象类中没有这样的方法,则序列化时默认调用是 ObjectOutputStream.defaultWriteObject() 方法,反序列化时默认调用ObjectInputStream.defaultReadObject() 方法。
ArrayList通过提供writeObject()和readObject()方法,只序列化了数组中有元素填充的那部分数据。
9. clone机制
ArrayList实现了Cloneable接口,重写了clone()方法。
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
ArrayList中的clone()方法返回的是一个全新的ArrayList实例对象,但是其elementData,也就是存储数据的数组,存储的对象还是指向了旧的ArrayList存储的那些对象。也就是ArrayList这个类实现了深拷贝,但是对于存储的对象还是浅拷贝。