ArrayList的类图
主要分析ArrayList常用方法和基本结构。
属性
// 默认大小
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组,后面解答两个空数组的作用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放数据的数组,后面解释为什么要加transient
transient Object[] elementData; // non-private to simplify nested class access
//当前list size
private int size;
//最大size
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
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)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
ArrayList提供了三个构造方法,当有参且size为0的时候,默认elementData = EMPTY_ELEMENTDATA, 当无参且size为0的时候默认elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA。要想明白为什么这样设计,需要阅读其他地方的代码才知道。
注:当未指定大小时,需要首次添加才进行扩容。
add方法
//添加在数据末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//指定位置添加,消耗资源比较大,需要将数组的元素向后移动
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++;
}
add方法的代码很简单,先判断是否需要扩容,然后在进行添加,添加到指定位置的数组则需要判断index是否超过最大(rangeCheckForAdd判断),然后在进行数组数据后移一位。
add操作离不开扩容,所以需要看看扩容相关代码
add 扩容相关
//方法1
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//方法2
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//方法3
private void grow(int minCapacity) {
// overflow-conscious code
//原本数组大小
int oldCapacity = elementData.length;
//新数组大小等于原来数组大小的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
//这里的意义是第一次初始化的时候oldCapacity为0,直接赋值为newCapacity。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//当已经达到最大的时候,会将newCapacity 赋值为Integer.MAX_VALUE,相比最大只多了8个位置,所以ArrayList最大存储为Integer.MAX_VALUE。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 这里实现扩容,底层还是使用的System.arraycopy进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
当我们构造ArrayList使用的是无参构造时,方法1中会将minCapacity(最小扩容大小)转换为10,然后传递到方法2,当没有使用无参构造,使用的是有参并且size等于0,方法1将不会对minCapacity进行修饰,此时minCapacity = 1;
方法2中如果构造的时候设置size = 0,minCapacity - elementData肯定是大于0,所以需要进行扩容处理,执行方法3进行扩容。方法2中的mocCount变量是继承自AbstractList,每次对list进行操作都会执行modCount++,用来在跌代器遍历时候判断操作是否合理。
方法3就是对ArrayList进行扩容。每次扩容的大小为原来数组大小的1.5倍,后面if考虑了1.5倍后是否存在精度丢失问题,因为使用的移位操作,所以不会报错。ArrayList可以存储的最大容量为Integer.MAX_VALUE
另:根据上面的扩容相关代码,可以解答一些前面的问题,将ArrayList中存储元素的内容设置为EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别是
- 当初始化为EMPTY_ELEMENTDATA时,第一次扩容为大小1。
- 当DEFAULTCAPACITY_EMPTY_ELEMENTDATA,第一次扩容大小为10。
第二个问题就是为什么最大值要设置为Integer.MAX_VALUE - 8,注释写的是因为有的虚拟机有一个对象头,占8个字节,所以预留了8个字节防止出现oom(数组也是一个对象)。
下面一段代码简易模拟了ArrayList扩容10次每一次的大小
int size = 10;
for (int i = 0; i < 10; i++) {
int minSize = (size + 1);
int newSize = size + (size >> 1);
if (newSize - minSize < 0)
newSize = size + 1;
if (newSize - (Integer.MAX_VALUE - 8) > 0)
newSize = Integer.MAX_VALUE;
size = newSize;
System.out.println("扩容后的大小" + size);
}
当初始size为10时
当初始size为0时
所以在开发过程中声明list时最好指定list的大小,尽量减少list的扩容。
remove
public E remove(int index) {
//判断index是否合理
rangeCheck(index);
//操作增加
modCount++;
//获取元素
E oldValue = elementData(index);
//定位需要移除元素下标
int numMoved = size - index - 1;
//如果在中间需要移动数组
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//最后一位置null,让gc回收
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
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;
}
remove 对象的时候是借助fastRemove方法,下面看出fastRemove执行的操作和根据索引remove操作差不多
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
}
addAll
public boolean addAll(Collection extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
好像没什么难度
杂记
ArrayList存储元素的数组使用transient 来禁止序列化,是因为ArrayList中有readObject和writeObject方法来写入和读取该数组。
关于ArrayList其他方法可以直接看看源码,整体来说阅读ArrayList的代码不是很难。
关于modCount变量,这个变量容器里面基本都有,这个变量很重要,它用来存储容器内的数据增删的操作,在进行迭代器遍历的时候这个参数用来判断是否在迭代器期间对容器进行了添加删除操作,从而抛出java.util.ConcurrentModificationException异常
关于ArrayList的迭代器这里也没有进行整理,因为篇幅太大,同时也没有难度,可以有需要的时候在进行阅读。