ArrayList源码分析
基于jdk1.7源码
一、源码分析
属性
//默认的初始容量大小,为10 private static final int DEFAULT_CAPACITY = 10; //Arraylist为空时,使用该共享的空数组 private static final Object[] EMPTY_ELEMENTDATA = {}; //用来存放元素的数组。 private transient Object[] elementData; //存放的元素个数 private int size;
①DEFAULT_CAPACITY:默认初始容量为10。
②EMPTY_ELEMENTDATA:表示空数组,
Arraylist在刚创建时通常是一个空数组,不含任何元素,如果一次创建了
③elementData:是用来缓存元素的数组,该属性被声明为transient。我们知道被声明为transient的属性在序列化时会被排除掉,Arraylist在序列化(已经实现了Serializable接口)时岂不是元素全部丢失了吗?
实际上ArrayList在序列化时会调用writeObject方法,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
④size:实际存放的元素的个数。
构造方法
/** * 构造器(指定初始容量) */ public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; } /** * 默认构造器 */ public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; } /** * Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator. 用指定容器中的元素来填充构造Arraylist。元素添加的顺序是指定容器的迭代器返回的元素的顺序 */ public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; // c.toArray might (incorrectly) not return Object[] (see 6260652) // toArray()方法不总是返回Object[] if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }
创建ArrayList时,如果使用默认构造器,则默认的初始容量值为10。也可以手动指定初始容量值,还可以用其它容器填充的方式来创建ArrayList。
注意:如果手动指定初始容量值,则该值既不能设置过大,也不能设置过小。如果过大,但元素增长过慢,则导致内存浪费。如果过小,则会造成频繁的扩容,而扩容时内部元素会进行移动,因此影响效率。所以需要根据具体的业务需求来指定合适的初始容量值。
add方法
//添加元素到尾部 public boolean add(E e) { ensureCapacityInternal(size + 1); // elementData[size++] = e; return true; }
添加元素之前,先使用ensureCapacityInternal确保数组有足够空间存储元素。来看看ensureCapacityInternal是如何实现的。
private void ensureCapacityInternal(int minCapacity) { if (elementData == 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);//*** } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //扩容了0.5倍容量 int newCapacity = oldCapacity + (oldCapacity >> 1);//>>表示右移一位,相当于除以2 //控制容量的上限和下限 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); }
扩充了多少容量呢?
//jdk7 int newCapacity = oldCapacity + (oldCapacity >> 1) //jdk6 int newCapacity = (oldCapacity * 3)/2 + 1;
(如果初始容量为10,JDK6扩容后为16,而JDK7扩容后为15,两者写法不同,但都大约扩容了0.5倍容量,也就是扩容到原容量的1.5倍)
这里我把jdk6中的相应代码也列出来了,两者写法略有不同。为什么jdk7会换了个写法?
其实原因很简单,因为oldCapacity直接乘以3(暴增),很可能会造成int溢出,而jdk7中的写法则是缓慢的增加,相比要安全的多。
更多关于ArrayList和Vector扩容1.5倍的讨论请参阅:
Logic used in ensureCapacity method in ArrayList
get方法
public E get(int index) { //先检查index是否越界 rangeCheck(index); return elementData(index); }
get方法非常简单,先判断下标是否越界,然后通过下标来获取元素。
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,元素个数减1 elementData[--size] = null; // 【clear to let GC do its work】 //返回删除的元素 return oldValue; }
如果删除的是末尾的元素,直接删除即可。而如果是其它位置,则需要将删除位后面的所有元素往前移动。
注意:
1.System.arraycopy()是一个native方法。
使用此方式拷贝数组比使用for循环的方式效率要高的多。
【疑问:System.arraycopy()是深克隆还是浅克隆?】
2.elementData[--size] = null这段代码有两层意思。
①删除元素后,末位元素已经没有存在的意义了,所以将其设为null。
②如果该元素是某个对象的引用,且不存在其它对该对象的引用,则设为null以便垃圾回收器尽早进行清理工作(不保证进行回收)
注意:如果是复杂类型,容器中存放的是对象的引用,所以删除时仅仅删除的是引用,真实的对象并未删除。
remove(Object o)方法
删除首次出现在ArrayList中的元素
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else {//非null则用equals来比较 for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; }
二、快速失败机制
请移步到fail-fast(快速失败/报错机制)-ConcurrentModificationException
总结
1.ArrayList底层使用对象数组实现。因为带有下标索引,所以随机访问速度快。而由于对数组的插入、移除、扩容等都需要进行元素的移动,所以相比基于链表实现的LinkedList,插入和移除操作速度慢。
插入、删除、扩容都需要进行元素的移动,是因为ArrayList底层是数组实现,数组一旦分配内存就不能发生改变,对ArrayList的插入、删除、扩容操作只是使用System.arraycopy(x…)重新生成了一个新的数组,然后将原有数组中的元素复制到新数组中。
2.ArrayList具有自动扩容机制。
我们在使用数组时通常会使用Arraylist来代替原生数组(例如int[]),这是因为ArrayList具有自动扩容机制,也就是当ArrayList中存放的元素个数达到了其容量之后,继续向其中添加元素,它会帮我们自动扩充容量。
3.ArrayList是非线程安全的。
ArrayList和Vector的比较请看Vector和ArrayList的比较
另外:需要搞清楚的问题:ArrayList(容器)中存放的是对象的引用还是对象本身?
如果是基本类型,存入的是变量的值。如果是复杂类型则存入的是对象的引用