4.Java集合-ArrayList实现原理及源码分析

一、ArrayList概述:

  ArrayList 是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存

  ArrayList不是线程安全的,只能用在单线程的情况下,多线程环境下可以考虑用Collections.synchronizedList(List  l)函数返回一个线程安全 和 ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList

  ArrayList实现了Serializable接口,因此它支持序列化,能够通过序列化传输,实现RandomAccess接口,支持快速随机访问,实际上就是通过下标序号进行快速访问,实现了Cloneable接口,能被克隆

  每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。 

  注意,此实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。

二、ArrayList的实现

  对于ArrayList而言,它实现List接口、底层使用数组保存所有元素。其操作基本上是对数组的操作。下面我们来分析ArrayList的源代码:

  1)私有属性:

  ArrayList只定义了两个私有属性:

 1 /** 
 2       * The array buffer into which the elements of the ArrayList are stored. 
 3       * The capacity of the ArrayList is the length of this array buffer. 
 4       */  
 5      private transient Object[] elementData;  
 6    
 7      /** 
 8       * The size of the ArrayList (the number of elements it contains). 
 9       * 
10       * @serial 
11       */  
12      private int size;

  很容易理解,elementData存储ArrayList内的元素,size表示它包含的元素的数量

  注:被标记为transient的属性在对象被序列化的时候不会被保存。

  

  2)构造方法:

    ArrayList提供了三种方式的构造器,可以1.构造一个默认容量为10的空列表、2.构造一个指定初始容量的空列表 3.以及构造一个包含指定collection元素的列表,这些元素按照该collection的迭代器返回他们的是顺序序列的。  

 1 // ArrayList带容量大小的构造函数。    
 2     public ArrayList(int initialCapacity) {    
 3         super();    
 4         if (initialCapacity < 0)    
 5             throw new IllegalArgumentException("Illegal Capacity: "+    
 6                                                initialCapacity);    
 7         // 新建一个数组    
 8         this.elementData = new Object[initialCapacity];    
 9     }    
10    
11     // ArrayList无参构造函数。默认容量是10。    
12     public ArrayList() {    
13         this(10);    
14     }    
15    
16     // 创建一个包含collection的ArrayList    
17     public ArrayList(Collection<? extends E> c) {    
18         elementData = c.toArray();    
19         size = elementData.length;    
20         if (elementData.getClass() != Object[].class)    
21             elementData = Arrays.copyOf(elementData, size, Object[].class);    
22     }

  

  3)元素存储

  ArrayList提供了set(int  index,E  element)、add(E  e)、add(int  index,E  element)、addAll(Collection<? extends E> c)、addAll(int index, Collection<? extends E> c)这些添加元素的方法。下面我们一一讲解:

 1 // 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。  
 2 public E set(int index, E element) {  
 3    RangeCheck(index);  
 4  
 5    E oldValue = (E) elementData[index];  
 6    elementData[index] = element;  
 7    return oldValue;  
 8 }    
 9 // 将指定的元素添加到此列表的尾部。  
10 public boolean add(E e) {  
11    ensureCapacity(size + 1);   
12    elementData[size++] = e;  
13    return true;  
14 }    
15 // 将指定的元素插入此列表中的指定位置。  
16 // 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。  
17 public void add(int index, E element) {  
18    if (index > size || index < 0)  
19        throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);  
20    // 如果数组长度不足,将进行扩容。  
21    ensureCapacity(size+1);  // Increments modCount!!  
22    // 将 elementData中从Index位置开始、长度为size-index的元素,  
23    // 拷贝到从下标为index+1位置开始的新的elementData数组中。  
24    // 即将当前位于该位置的元素以及所有后续元素右移一个位置。  
25    System.arraycopy(elementData, index, elementData, index + 1, size - index);  
26    elementData[index] = element;  
27    size++;  
28 }    
29 // 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。  
30 public boolean addAll(Collection<? extends E> c) {  
31    Object[] a = c.toArray();  
32    int numNew = a.length;  
33    ensureCapacity(size + numNew);  // Increments modCount  
34    System.arraycopy(a, 0, elementData, size, numNew);  
35    size += numNew;  
36    return numNew != 0;  
37 }    
38 // 从指定的位置开始,将指定collection中的所有元素插入到此列表中。  
39 public boolean addAll(int index, Collection<? extends E> c) {  
40    if (index > size || index < 0)  
41        throw new IndexOutOfBoundsException(  
42            "Index: " + index + ", Size: " + size);  
43  
44    Object[] a = c.toArray();  
45    int numNew = a.length;  
46    ensureCapacity(size + numNew);  // Increments modCount  
47  
48    int numMoved = size - index;  
49    if (numMoved > 0)  
50        System.arraycopy(elementData, index, elementData, index + numNew, numMoved);  
51  
52    System.arraycopy(a, 0, elementData, index, numNew);  
53    size += numNew;  
54    return numNew != 0;  
55    }

  书上都说ArrayList是基于数组实现的,属性中也看到了数组,具体是怎么实现的呢?比如就这个添加元素的方法,如果数组大,则在将某个位置的值设置为指定元素即可,如果数组容量不够了呢?

  看到add(E e)中先调用了ensureCapacity(size+1)方法,之后将元素的索引赋给elementData[size],而后size自增。例如初次添加时,size为0,add将elementData[0]赋值为e,然后size设置为1(类似执行以下两条语句elementData[0]=e;size=1)。将元素的索引赋给elementData[size]不是会出现数组越界的情况吗?这里关键就在ensureCapacity(size+1)中了。

4) 元素读取:

1  // 返回此列表中指定位置上的元素。  
2  public E get(int index) {  
3     RangeCheck(index);  
4   
5     return (E) elementData[index];  
6   }

5) 元素删除:ArrayList提供了根据下标或者指定对象两种方式的删除功能。如下:remove(int index):

 1 // 移除此列表中指定位置上的元素。  
 2  public E remove(int index) {  
 3     RangeCheck(index);  
 4   
 5     modCount++;  
 6     E oldValue = (E) elementData[index];  
 7   
 8     int numMoved = size - index - 1;  
 9     if (numMoved > 0)  
10         System.arraycopy(elementData, index+1, elementData, index, numMoved);  
11     elementData[--size] = null; // Let gc do its work  
12   
13     return oldValue;  
14  }

  首先是检查范围,修改modCount,保留将要被移除的元素,将移除位置之后的元素向前挪动一个位置,将List末尾元素置为空(null),返回被移除的元素。

  

  remove(Object  o)

 1 // 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。  
 2  public boolean remove(Object o) {  
 3     // 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。  
 4     if (o == null) {  
 5         for (int index = 0; index < size; index++)  
 6             if (elementData[index] == null) {  
 7                 // 类似remove(int index),移除列表中指定位置上的元素。  
 8                 fastRemove(index);  
 9                 return true;  
10             }  
11     } else {  
12         for (int index = 0; index < size; index++)  
13             if (o.equals(elementData[index])) {  
14                 fastRemove(index);  
15                 return true;  
16             }  
17         }  
18         return false;  
19     } 
20 }

  首先通过代码可以看到,当移除成功后返回true,否则返回false。remove(Object o)中通过遍历element寻找是否存在传入对象,一旦找到就调用fastRemove移除对象。为什么找到了元素就知道了index,不通过remove(index)来移除元素呢?因为fastRemove跳过了判断边界的处理,因为找到元素就相当于确定了index不会超过边界,而且fastRemove并不返回被移除的元素。下面是fastRemove的代码,基本和remove(index)一致。

1 private void fastRemove(int index) {  
2          modCount++;  
3          int numMoved = size - index - 1;  
4          if (numMoved > 0)  
5              System.arraycopy(elementData, index+1, elementData, index,  
6                               numMoved);  
7          elementData[--size] = null; // Let gc do its work  
8  }

removeRange(int fromIndex,int toIndex)

 1 protected void removeRange(int fromIndex, int toIndex) {  
 2      modCount++;  
 3      int numMoved = size - toIndex;  
 4          System.arraycopy(elementData, toIndex, elementData, fromIndex,  
 5                           numMoved);  
 6    
 7      // Let gc do its work  
 8      int newSize = size - (toIndex-fromIndex);  
 9      while (size != newSize)  
10          elementData[--size] = null;  
11 }

  执行过程是将elementData从toInedx位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size

  6) 调整数组容量ensureCapacity:

    从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超过当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int  minCapacity)来实现。在实际添加大量元素前,也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量

 1 public void ensureCapacity(int minCapacity) {  
 2     modCount++;  
 3     int oldCapacity = elementData.length;  
 4     if (minCapacity > oldCapacity) {  
 5         Object oldData[] = elementData;  
 6         int newCapacity = (oldCapacity * 3)/2 + 1;  //增加50%+1
 7             if (newCapacity < minCapacity)  
 8                 newCapacity = minCapacity;  
 9       // minCapacity is usually close to size, so this is a win:  
10       elementData = Arrays.copyOf(elementData, newCapacity);  
11     }  
12  }

  从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量

  注:Object oldData[] = elementData;//为什么要用到oldData[],乍一看来后面并没有用到关于oldData, 这句话显得多此一举!

  因为这样证明这块老的内存依然有引用,分配内存的时候就不会被侵占掉,然后copy完成后这个局部变量的生命期也过去了,然后释放才是安全的,保证后面的copy操作完成之前,不会被gc回收

关于ArrayList和Vector区别如下:

  1.ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。

  2.Vector提供indexOf(obj, start)接口,ArrayList没有。

  3.Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为线程安全需要更大的系统开销

 

ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:

public void trimToSize() {  
   modCount++;  
   int oldCapacity = elementData.length;  
   if (size < oldCapacity) {  
       elementData = Arrays.copyOf(elementData, size);  
   }  
}

  由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length和size相同,节省空间

7)转为静态数组toArray

  第一个, 调用Arrays.copyOf将返回一个数组,数组内容是size个elementData的元素,即拷贝elementData从0至size-1位置的元素到新数组并返回。

public Object[] toArray() {  
         return Arrays.copyOf(elementData, size);  
 } 

  

  第二个,如果传入数组的长度小于size,返回一个新的数组,大小为size,类型与传入数组相同。所传入数组长度与size相等,则将elementData复制到传入数组中并返回传入的数组。若传入数组长度大于size,除了复制elementData外,还将把返回数组的第size个元素置为空。

public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
}

Fail-Fast机制: 

  ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败(抛出ConcurrentModificationException),而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

三、总结

关于ArrayList的源码,给出几点比较重要的总结:

  1.注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数的构造方法,将Collection 转化为数组赋给ArrayList实现数组 elementData。

  2.注意倥偬容量的方法 ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都需要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置成新的容量为旧的容量的1.5倍 + 1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后勇Arrays.copy()方法将元素宝贝到新的数组。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常耗时,也因此建议在事先能够确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。

  3.arrayList的实现中大量地调用了ArrayList.copyof() 和 System.arraycopy() 方法。我们有必要对这两个方法的实现做下深入的了解

  首先来看Arrays.copyof() 方法。它有很多个重载的方法,但实现的思路是一样的

1 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {  
2     T[] copy = ((Object)newType == (Object)Object[].class)  
3         ? (T[]) new Object[newLength]  
4         : (T[]) Array.newInstance(newType.getComponentType(), newLength);  
5     System.arraycopy(original, 0, copy, 0,  
6                      Math.min(original.length, newLength));  
7     return copy;  
8 }

  Arrays.copyof()方法 实际上是在其内部有创建了一个长度为 newlength的数组,调用System.arraycopy()方法,将原来数组中的元素复制到了新的数组中。

  下面来看System.arraycopy() 方法,该方法被标记了native,调用了系统的 C / C++代码,在JDK中是看不倒,但在openJDK中可以看到其源码。该函数实际上最终调用了C语言的memmove()函数,一次它可以保证同一个数组内元素的正确复制和移动,比一般的复制方法的实现效率要高很多,很时候用来批量处理数据。Java强烈推荐在复制大量数组元素时使用该方法,以取得更高的效率。

  4、ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。

  5、在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。

转载自:http://www.cnblogs.com/ITtangtang/p/3948555.html

  

原文地址:https://www.cnblogs.com/xuzekun/p/7462037.html