ArrayList源码剖析

ArrayList源码剖析

         ArrayList是我们常用的线性表实现之一,其底层的数据结构是数组。ArrayList实现了List, RandomAccess, Cloneable, 以及Serializable接口,说明ArrayList支持List的基本操作,能随机访问,以及支持深克隆和序列化。与LinkedList不同的是,ArrayList没有实现Deque接口,说明Java集合框架没有将ArrayList作为队列以及栈的实现类,这也是很显然的,因为栈和队列的特点就是频繁的增删操作,使用链表的性能大多数情况下是极大优于数组的。下面就从属性,构造器以及几个基本的方法去剖析ArrayList的源码。

1. 基本的属性概括

         ArrayList的基本属性包括下面几个:

 1 //默认的初始数组容量(最大长度)
 2 private static final int DEFAULT_CAPACITY = 10;
 3 //当数组容量为0的时候的默认数组(为什么要声明为static)
 4 private static final Object[] EMPTY_ELEMENTDATA = {};
 5 //当数组容量为默认容量时候的默认数组,防止浪费内存空间,所以初始为空,当添加元素时根据默认容量和最少需要的容量哪个大来得到实际容量并生成新的数组。
 6 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 7 //ArrayList实际的数据结构形式,一个Object数组
 8 transient Object[] elementData;
 9 //ArrayList的大小
10 private int size;

2. 构造器

         ArrayList提供了三种构造器。与LinkedList一样,ArrayList也有无参构造器和参数为集合的构造器,同时ArrayList也提供了参数为整形的构造器,下面是这三个构造器的源码分析。

         首先是带有整形参数的构造器,参数表示初始容量。这段代码的逻辑比较简单,判断初始容量参数是否大于等于0,如果小于0则抛出非法参数异常,如果等于0,则当前对象的数组引用静态的空数组,如果大于0,则new一个容量为初始容量参数的Object数组,完成初始化。

 1 public ArrayList(int initialCapacity) {
 2         if (initialCapacity > 0) {
 3             this.elementData = new Object[initialCapacity];
 4         } else if (initialCapacity == 0) {
 5             this.elementData = EMPTY_ELEMENTDATA;
 6         } else {
 7             throw new IllegalArgumentException("Illegal Capacity: "+
 8                                                initialCapacity);
 9         }
10     }

         其次是无参的构造器。我们会惊讶的发现,无参构造器并不是直接new一个容量为默认容量的数组,而是引用了静态的默认容量空数组,之所以这么做我想是因为,由无参构造器产生的ArrayList对象不一定会有元素在其中,为了节省内存空间,先使用静态的默认容量空数组,当添加元素的时候再判断是否进行对elementData进行扩容。

1     public ArrayList() {
2         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
3     }

         最后是参数为集合的构造器。泛型的extends保证了集合参数的泛型是当前ArrayList的泛型的类或者其子类。这段代码说明,集合参数通过toArray方法转换为Object数组,之后根据集合参数的大小,如果等于0,则elementData引用静态的空数组,否则再判断elementData是否是Object[]类型(为什么?),如果不是Object[]类型,则调用copyOf方法以Object[]对象的形式复制会elementData

 1 public ArrayList(Collection<? extends E> c) {
 2         elementData = c.toArray();
 3         if ((size = elementData.length) != 0) {
 4             // c.toArray might (incorrectly) not return Object[] (see 6260652)
 5             if (elementData.getClass() != Object[].class)
 6                 elementData = Arrays.copyOf(elementData, size, Object[].class);
 7         } else {
 8             // replace with empty array.
 9             this.elementData = EMPTY_ELEMENTDATA;
10         }
11     }

3. add(E)以及add(int, E)方法

         在调用add(E e)方法添加元素的时候,首先检查当添加元素之后的大小是否大于当前数组的大小,如果大于则调用grow(int minCapaciy)方法进行数组的扩容, 否则将新的元素保存在size下标的位置,size自增一。

         概括来说,ArrayList的扩容操作目的是确保数组的容量要满足至少能存下minCapacity个元素,为了达到这个目的,不是直接将新容量设置成minCapacity,而是先尝试在原容量的基础上计算新容量,计算公式为newCapacity = 1.5 * oldCapacity。如果这个新的容量无法满足目的,才将新容量设置成minCapacity,为什么不直接将新容量设置成minCapacity而是先根据旧容量计算新容量,这个问题我也不是很清楚,查阅了外网的资料和sun javadoc,比较被认可的说法是这个操作是一种安全的赌博(safe bet)。

 1 public boolean add(E e) {
 2         ensureCapacityInternal(size + 1);  // Increments modCount!!
 3         elementData[size++] = e;
 4         return true;
 5     }
 6 
 7 private void grow(int minCapacity) {
 8         // overflow-conscious code
 9         int oldCapacity = elementData.length;
10         int newCapacity = oldCapacity + (oldCapacity >> 1);
11         if (newCapacity - minCapacity < 0)
12             newCapacity = minCapacity;
13         if (newCapacity - MAX_ARRAY_SIZE > 0)
14             newCapacity = hugeCapacity(minCapacity);
15         // minCapacity is usually close to size, so this is a win:
16         elementData = Arrays.copyOf(elementData, newCapacity);
17     }

         add(ine index, E element)方法是将元素插入到指定的下标Index处。首先先检查index是否合法,之后跟add(E e)一样,确保数组容量足够,不同的地方在于,如果是在特定下标index处添加元素,则原本的[index, size-1]下标的元素要全部向后移一个位置,即对应的移动到[index + 1, size' - 1],注意size'=size + 1。

1 public void add(int index, E element) {
2         rangeCheckForAdd(index);
3 
4         ensureCapacityInternal(size + 1);  // Increments modCount!!
5         System.arraycopy(elementData, index, elementData, index + 1,
6                          size - index);
7         elementData[index] = element;
8         size++;
9     }

4. remove(int)和remove(Object)方法

         remove(int index)删除特定下标的元素,并将被删除的元素返回。方法首先检查index是否合法,之后调用elementData(int index)返回特定下标的元素,而不是直接用elementData[index]返回元素。之后要把index之后的所有元素前移一个位置,同样是调用arraycopy的进行数组的复制。最后将数组最后的元素的下一个位置置为空方便GC回收。

 1 public E remove(int index) {
 2         rangeCheck(index);
 3 
 4         modCount++;
 5     //之所以需要elementData(int index)方法来读取特定下标元素,是因为读操作和写操作不同。ArrayList的底层是Object数组,写操作是绝对安全的(所有的类父类都是Object,所以Object可以引用任何类)。但是读操作不同,读取返回的类型是泛型E,无法确保Object到E的类型转换是安全的,这时候编译器就会发出警告。如果在每一个需要读取元素的地方都是用element[index]的方式获取元素,则编译器会发出大量的警告,不方便我们调试并且看着心烦,如果我们将element[index]封装到一个方法里,在方法里进行强制类型转换,并且用 @SuppressWarnings("unchecked")让编译器闭嘴,则会更美观些哈哈。
 6         E oldValue = elementData(index);
 7 
 8         int numMoved = size - index - 1;
 9         if (numMoved > 0)
10             System.arraycopy(elementData, index+1, elementData, index,
11                              numMoved);
12         elementData[--size] = null; // clear to let GC do its work
13 
14         return oldValue;
15     }

         remove(Object o)方法与根据下标删除的方法不同,其删除之后不会将被删除的元素返回。这个方法从前向后遍历数组,删除遇到的第一个等于参数o的元素,这里的等于形式的说法是这样的,(o==null)?get(i)==null:o.equals(get(i))。

 1 public boolean remove(Object o) {
 2         if (o == null) {
 3             for (int index = 0; index < size; index++)
 4                 if (elementData[index] == null) {
 5                     //fastRemove与普通remove不同之处在于其不进行边界检查,因为循环是从0到size-1,肯定不会删到边界之外的元素。
 6                     fastRemove(index);
 7                     return true;
 8                 }
 9         } else {
10             for (int index = 0; index < size; index++)
11                 if (o.equals(elementData[index])) {
12                     fastRemove(index);
13                     return true;
14                 }
15         }
16         return false;
17     }

5. set(int, E)方法

         set(int index, E element)方法将下标为index的元素修改为element,并返回旧的元素。

1 public E set(int index, E element) {
2     //下标检查, 如果index >= size,则报IndexOutOfBoundsException。为什么不检查index是否为负数呢?因为如果index为负数,则之后的数组访问操作必然会抛出ArrayIndexOutOfBoundsException,没必要重复编码来抛这个异常。
3     //IndexOutOfBoundsException和ArrayIndexOutOfBoundsException的关系和区别是,IndexOutOfBoundsException是发生于例如字符串,数组,以及其他集合在下标超出范围的时候,是ArrayIndexOutOfBoundsException的父类,ArrayIndexOutOfBoundsException是其具体实现,只针对于访问数组的下标为负数或者大于等于数组大小的情况。
4         rangeCheck(index);
5 
6         E oldValue = elementData(index);
7         elementData[index] = element;
8         return oldValue;
9     }

 

posted @ 2021-06-19 16:31  龙刃已准备出鞘  阅读(24)  评论(0编辑  收藏  举报