ArrayList的类声明
首先看一下ArrayList的类声明
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable,
java.io.Serializable
ArrayList类主要是继承AbstractList类并实现了List接口,实现Cloneable,Serializable接口,使得ArrayList具有克隆,序列化功能。
而实现RandomAccess接口使得ArrayList具有快速访问,即通过下标访问的功能。
ArrayList的属性
private static final long serialVersionUID = 8683452581122892189L;
transient Object[] elementData;
// non-private to simplify nested class access
首先我们看到serialVersionUID的声明,为了保持序列化的兼容性,
参考:Effective Java之谨慎地实现Serializable(七十四)
elementData数组用来存储ArrayList中的元素,从这个可以看出,ArrayList是底层是借组于数组来实现的。为什么声明为transient呢?
是因为避免默认的序列化带来的内存浪费,参考:Effective Java之考虑自定义的序列化模式(七十五)
有关容器类的序列化问题在后面的容器源码中就不赘述了,无非就是把默认的序列化不满足需求,自定义一个序列化。
private int size;
private static final int DEFAULT_CAPACITY = 10;
size用来记录ArrayList中存储的元素的个数。
DEFAULT_CAPACITY是ArrayList中存储的元素的默认个数,默认是10。
private static final Object[] EMPTY_ELEMENTDATA = {};
//下面这个是共享空常量数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
这两个是共享空常量数组,用于空的实例对象,区别是DEFAULTCAPACITY_EMPTY_ELEMENTDATA知道如何扩张;
(这里先知道他们的区别,至于为什么看到后面就知道了)
protected transient int modCount = 0;
使用迭代器遍历的时候,用来检查列表中的元素是否发生结构性变化(列表元素数量发生改变)了,主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构,如果这种情况发生,抛出
ConcurrentModificationException,实现了快速失败(fail-fast)。
ArrayList的构造函数
1.无参构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
源码上介绍的功能为:构造一个初始容量为 10 的空列表。
DEFAULTCAPACITY_EMPTY_ELEMENTDATA的长度为什么是10呢?在下面的add(E e)函数源码的介绍中会给出答案。
2.指定容量构造方法
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);
}
}
根据参数的大小作为容量来实例化底层的数组对象,其中对参数的3中情况进行了处理。当参数小于0时,抛异常。当参数等于0时,用空的常量数组对象EMPTY_ELEMENTDATA来初始化底层数组elementData。
3.容器作为参数的构造函数
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,还对Collection转化是否转化为了Object[]进行了检查判断。如果Collection为空,则就将空的常量数组对象EMPTY_ELEMENTDATA赋给了elementData;
Add的所有方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// Increments modCount!!
elementData[size++] = e;
return true;
}
既然此函数中涉及到ensureCapacityInternal函数,那我们就看一下这个函数,源码如下:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
继续看
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
从源码可以看出,如果elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则就用默认容量10来进行开辟空间。这里的源码就解释了DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组和EMPTY_ELEMENTDATA数组的区别之所在。
当我们用无参构造函数来实例化一个对象时,只需要调用一次add(E e) 就能构造的一个长度为10的数组对象。
小小程序看出端倪:
public static void main(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>(0);
ArrayList<Integer> list2 = new ArrayList<>();
list1.add(3);
list2.add(3);
System.out.println(list1.get(3));
}
debug看到的两个对象区别
![](/Users/yj/Desktop/屏幕快照 2018-01-08 下午9.53.24.png)
![](/Users/yj/Desktop/屏幕快照 2018-01-08 下午9.53.42.png)
if (minCapacity - elementData.length > 0)
grow(minCapacity);
当添加的元素数目大于了数组elementData,就grow:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
/*
下面两个if的作用为处理两种情况:
1)第一种情况为:如果newCapacity扩展的过小。则应该至少扩张到所需的空间大小minCapacity
2)第二种情况为:newCapacity扩张的过大,如果过大,则用Integer.MAX_VALUE来代替。
*/
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
int newCapacity = oldCapacity + (oldCapacity >> 1);
这段代码其实就是把elementData的大小扩展成原来的1.5倍,细心的人会发现,如果是elementData通过无参构造方法赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组那么它将从10开始扩张。
EMPTY_ELEMENTDATA的话它将从2开始扩张。
有存在一个小疑点:
elementData = Arrays.copyOf(elementData, newCapacity);
实际上最终调用到一个系统方法
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
这是因为C在操作内存的效率远高于Java。所以在许多优化文档中都能看见推荐使用arraycopy方法来批量处理数组。
终于把add方法有了初步的了解,其他带参数的add方法其实都一样,无非就是
- 检查参数的有效性
- 添加addmode,并检查是否需要扩张
添加元素
其他方法
为什么将其他方法放在同一栏中呢?因为so ez
首先get和set方法就不必多言了,不用看都知道怎么做吧?
remove方法需要注意一下,
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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
其实也就是将数组从index+1位置开始到末尾的元素拷贝到从index开始处)
也并不是什么高明的方法。
另一个remove的重载方法:
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;
}
fastRemove(index)?难道是很高明的方法?马上去瞧瞧
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
}
其实区别不大,区别是fastRemove函数没有对index进行有效性检查,以及没有返回移除的旧值,为什么?想想就知道了~
需要注意的地方:
第一点:有3种遍历数组的方法,iterator迭代器,用array.get随机访问,用for-each循环,但是使用随机访问(即,通过索引序号访问)效率最高,而使用迭代器的效率最低!
第二点:ArrayList提供了2个toArray()函数:
Object[] toArray()
<T> T[] toArray(T[] contents)
调用 toArray() 函数会抛出“java.lang.ClassCastException”异常,但是调用 toArray(T[] contents) 能正常返回 T[]。
toArray() 会抛出异常是因为 toArray() 返回的是 Object[] 数组,将 Object[] 转换为其它类型(如如,将Object[]转换为的Integer[])则会抛出“java.lang.ClassCastException”异常,因为Java不支持向下转型。
解决该问题的办法是调用 T[] toArray(T[] contents) , 而不是 Object[] toArray()。
public static Integer[] vectorToArray2(ArrayList<Integer> v) {
Integer[] newText = (Integer[])v.toArray(new Integer[0]);
return newText;
}