ArrayList是应用最广泛的线性表之一

特点:

  • 线程非安全(安全版本的ArrayList是Vector)
  • 容量无限(最大为Integer.MAX_VALUE),动态扩容
  • 逻辑上内存分配是连续的,有数据下标,取数据时间复杂度为O(1),添加数据有可能会导致扩容,时间复杂度<=O(n)
    底层采用的是System.arraycopy的本地方法

成员变量

//默认容量为10个
private static final int DEFAULT_CAPACITY = 10;
//空对象
private static final Object[] EMPTY_ELEMENTDATA = {};
//空对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//底层数据容器是Object数组
transient Object[] elementData;
//容器大小
private int size;

构造函数

//给定一个初始容量,如果知道数组大概长度的情况下可用此构造函数,
//避免ArrayList频繁的扩容带来的性能损耗
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);
	}
}
//默认无参构造函数
public ArrayList() {
	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//用另外的容器来初始化
public ArrayList(Collection<? extends E> c) {
	elementData = c.toArray();
	if ((size = elementData.length) != 0) {
	    // c.toArray might (incorrectly) not return Object[] (see 6260652)
	    //这里需要注意一下:c.toArray有可能返回的不是Object[],此时用Arrays.copyOf将数组中的元素再拷贝一遍
		//比如Mylist extends ArrayList,然后重写toArray返回非Object[]
		//具体原因看这里https://blog.csdn.net/GuLu_GuLu_jp/article/details/51457492
		if (elementData.getClass() != Object[].class)
			elementData = Arrays.copyOf(elementData, size, Object[].class);
	} else {
		// 如果给一个空容器则用空对象初始化
		this.elementData = EMPTY_ELEMENTDATA;
	}
}

添加元素

尾部追加

public boolean add(E e)其实就类似于append,在尾部追加

//add 方法看似简单,只有三句话,其实每次都会去检查数组是否需要扩容
public boolean add(E e) {
	//确定是否需要扩容,因为新的元素进来了,所以需要size + 1
	//相当于确定在满足需求的容量下是否需要扩容
	ensureCapacityInternal(size + 1);  // Increments modCount!!
	//赋值元素
	elementData[size++] = e;
	return true;
}

private void ensureCapacityInternal(int minCapacity) {
	//如果是第一次添加元素,设置minCapacity为默认的容量10(DEFAULT_CAPACITY)
	//minCapacity(大于默认容量10时候)可以理解为要装下所有元素(包括即将要装进来的)所需要的容量
	if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
		minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
	}
	ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
	modCount++;
	//如果装下所有元素需要的容量大于了底层数组的长度
	//也就是说底层数据装不下新元素了,那么就需要扩容
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}

private void grow(int minCapacity) {
	int oldCapacity = elementData.length;
	//oldCapacity >> 1 表示oldCapacity右移一位相当于oldCapacity除以2^1
	//所以每次扩容都是在原来容量的1.5倍
	int newCapacity = oldCapacity + (oldCapacity >> 1); 
	//首次扩容时,会走这个条件,相当于扩容到10
	if (newCapacity - minCapacity < 0)
		newCapacity = minCapacity;
	//判断是否超过最大限制
	//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,为什么要减8呢,可以看看这里
	//https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8
	if (newCapacity - MAX_ARRAY_SIZE > 0)
		newCapacity = hugeCapacity(minCapacity);
	//扩容操作,底层调用的是System.arraycopy,native方法,内存操作,效率高
	//此时会new一个新的数组,并将原来数组里面的元素挨个拷贝过去
	//所以说如果经常扩容会影响效率
	elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
	//这里有个疑惑的地方,从逻辑来看minCapacity不可能会小余0
	//而且jdk源码注释这里是overflow,这里要怎么理解?
	if (minCapacity < 0) // overflow
		throw new OutOfMemoryError();
	return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

某位置上添加

public void add(int index, E element)频繁的调用不是明智之举,因为每往数组中间添加一个元素,都会把数组掰成两半,然后在中间放入元素,会影响该元素后的所有元素(内存表现为往右边移动一个位置)

public void add(int index, E element) {
	//检查索引是否超过范围
	rangeCheckForAdd(index);
	//扩容操作,和上面一样
	ensureCapacityInternal(size + 1);  // Increments modCount!!
	//核心代码,将index位置后面的数据挨个"顺移",空出索引位置再将元素赋值给当前索引
	//频繁的"顺移"不是明智之举,这也是ArrayList"写慢"的原因
	System.arraycopy(elementData, index, elementData, index + 1,size - index);
	elementData[index] = element;
	size++;
}
//检查索引范围,没什么好说的
private void rangeCheckForAdd(int index) {
	if (index > size || index < 0)
		throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

#令人疑惑的minCapacity < 0
逻辑上来看minCapacity是不可能小余0的,但是忽略了一种情况,ArrayList的size是用int类型来表示,如果把两个很大的ArrayList通过addAll合并成一个,size超出了int的范围,则有可能导致minCapacity小余0,抛出outOfMemory错误
参考:https://stackoverflow.com/questions/8557832/cant-understand-the-possibility-of-overflow-when-resizing-arraylist-in-java

private static int hugeCapacity(int minCapacity) {
	//令人疑惑的代码
	if (minCapacity < 0) // overflow
		throw new OutOfMemoryError();
	return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}

//看一下addAll代码你就明白了
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
	//新容器元素的数量
    int numNew = a.length;
	//当size和numNew的和超过了int表示的范围但是又强转int所以会出现负数
	//可以试一下:System.out.println(Integer.MAX_VALUE + 1);
    ensureCapacityInternal(size + numNew);
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

包含元素的方法contains

public boolean contains(Object o) {
	return indexOf(o) >= 0;
}
public int indexOf(Object o) {
    //判断元素是否为null,分别进行处理
	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;
}

删除元素的方法

有两种,一种是移除指定位置上的元素,一种是移除指定内容(Object)元素

//移除指定位置(索引)上的元素
public E remove(int index) {
	//检查索引是否越界
	rangeCheck(index);
	//更改"被修改"次数
	modCount++;
	//拿出数据用于返回
	E oldValue = elementData(index);
	//该位置后面还有多少个元素(将要被移动的元素)
	int numMoved = size - index - 1;
	if (numMoved > 0)
		//从被移除元素的下一个位置开始,将后面剩下的所有元素往前挪一个位置,
		System.arraycopy(elementData, index+1, elementData, index,numMoved);//五个参数分别是src srcPos dest destPos length
	//将最后一个位置上的元素置空	
	elementData[--size] = null; // clear to let GC do its work
	return oldValue;
}
//移除指定元素
public boolean remove(Object o) {
    //null和非null分开处理
	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;
}
//fastRemove和remove相比只是少了边界检查rangeCheck
private void fastRemove(int index) {
	modCount++;
	int numMoved = size - index - 1;
	if (numMoved > 0)
		System.arraycopy(elementData, index+1, elementData, index,numMoved);
	elementData[--size] = null; // clear to let GC do its work
}

关于modCount

很多地方都看到对modCount的修改,比如扩容操作ensureExplicitCapacity,或者移除元素remove的时候,modCount意味结构性修改,比如修改列表的长度,这个用来检查列表是否被修改过,因为ArrayList是非线程安全的,所以通过modCount来确定列表是否被修改,如果被意外的修改,则会抛出ConcurrentModificationException