1. 程式人生 > 實用技巧 >資料結構線性表之順序表ArrayList

資料結構線性表之順序表ArrayList

一、什麼是順序表

  在瞭解順序表之前,需要先了解什麼是順序表?順序表是線性表的一種,線性表分為順序表和連結串列。其中順序表在java中又分為陣列(最簡單的順序表)和ArrayList。為什麼我們稱其為順序表呢?原因顧名思義是該資料結構在邏輯上和物理結構上的儲存順序均是連續的。下面,我們就以一張圖來說明什麼是順序表。

這個圖代表邏輯上的9個元素,每一個位置均儲存一個數據,資料儲存在記憶體中時的實體地址也是連續的。

下面我們就介紹一下ArrayList的基本操作,增刪改查。

二、ArrayList的資料格式

  在此篇文章中,我們只講ArrayList,陣列不再講解。ArrayList為什麼說是順序表呢?其原因我們可以看到ArrayList內部維護的其實還是一個快取陣列,所有的操作都是基於該陣列進行實現。看原始碼:

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     
*/ transient Object[] elementData; // non-private to simplify nested class access

三、ArrayList的擴容

  ArrayList有一套自己的擴容機制,在ArrayList初始化完成之後,如果不指定容量,則快取容量預設為10.

1     /**
2      * Default initial capacity.
3      */
4     private static final int DEFAULT_CAPACITY = 10;

  在每次add操作時,會進行擴容計算,整個流程原始碼如下:

 1
ensureCapacityInternal(size + 1); // Increments modCount!! 新增元素時呼叫擴容 2 3 private void ensureCapacityInternal(int minCapacity) { 4 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); 5 } 6 7 private static int calculateCapacity(Object[] elementData, int minCapacity) { // 得到最小的擴容量 8 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 9 return Math.max(DEFAULT_CAPACITY, minCapacity); // 獲取預設的容量和傳入引數較大的一個值 10 } 11 return minCapacity; 12 } 13 14 private void ensureExplicitCapacity(int minCapacity) { 15 modCount++; 16 17 // overflow-conscious code 18 if (minCapacity - elementData.length > 0) 19 grow(minCapacity); // 擴容開始 20 } 21 22 /** 23 * Increases the capacity to ensure that it can hold at least the 24 * number of elements specified by the minimum capacity argument. 25 * 26 * @param minCapacity the desired minimum capacity 27 */ 28 private void grow(int minCapacity) { 29 // overflow-conscious code 30 int oldCapacity = elementData.length; 31 int newCapacity = oldCapacity + (oldCapacity >> 1); //右移操作,相當於oldCapacity/2 擴容之後為原來的1.5倍,也稱之為擴容係數 32 if (newCapacity - minCapacity < 0) // 新容量時否小於最小需要的容量,小於則新容量為最小需要容量 33 newCapacity = minCapacity; 34 if (newCapacity - MAX_ARRAY_SIZE > 0) //新容量比最大陣列容量還大,則需要判斷 35 newCapacity = hugeCapacity(minCapacity); 36 // minCapacity is usually close to size, so this is a win: 37 elementData = Arrays.copyOf(elementData, newCapacity); 38 } 39 40 private static int hugeCapacity(int minCapacity) { 41 if (minCapacity < 0) // overflow 42 throw new OutOfMemoryError(); 43 return (minCapacity > MAX_ARRAY_SIZE) ? 44 Integer.MAX_VALUE : 45 MAX_ARRAY_SIZE; 46 }

我們分析一下該擴容機制:

  (1)當我們add第一個元素時,此時陣列初始化(無參構成時)的還是

      DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}

    則此時

      minCapacity = DEFAULT_CAPACITY //10
    
    因此
      ensureExplicitCapacity(int minCapacity)方法中 10 - 0 > 0的,所以會進行第一次擴容,進入到grow(int minCapacity)方法之後,先獲取到oldCapacity為0,newCapacity通
     過位移執行也為0,則newCapacity=minCapacity=10;
容量確定為10擴容操作copyOf。
  (2)當第二次add元素操作時,minCapacity 為2,此時elementData.length(容量)在新增第一個元素後擴容成 10 了。此時,minCapacity - elementData.length > 0 不成立,所以不
    會進入 grow(minCapacity) 方法。所以就不會擴容,直至到新增第11個元素時,minCapacity(11) > elementData.length(10),進入grow方法進行擴容。進入grow方法之後,
    newCapacity變為10 + 10 >> 1 = 15,此時15大於最小需要容量,則將陣列擴容為15。以此類推下去進行擴容.....

四、ArrayList的add()方法

  眾所周知,ArrayList適合查詢操作,問為什麼?肯定都會說效果高(因為它支援下標隨機訪問),那新增操作呢?效率不高嗎?那我們帶著這些疑問來一起看看原始碼。

 1     /**
 2      * Appends the specified element to the end of this list.
 3      *
 4      * @param e element to be appended to this list
 5      * @return <tt>true</tt> (as specified by {@link Collection#add})
 6      */
 7     public boolean add(E e) {
 8         ensureCapacityInternal(size + 1);  // Increments modCount!!
 9         elementData[size++] = e;
10         return true;
11     }
 1     /**
 2      * Inserts the specified element at the specified position in this
 3      * list. Shifts the element currently at that position (if any) and
 4      * any subsequent elements to the right (adds one to their indices).
 5      *
 6      * @param index index at which the specified element is to be inserted
 7      * @param element element to be inserted
 8      * @throws IndexOutOfBoundsException {@inheritDoc}
 9      */
10     public void add(int index, E element) {
11         rangeCheckForAdd(index);
12 
13         ensureCapacityInternal(size + 1);  // Increments modCount!!
14         System.arraycopy(elementData, index, elementData, index + 1,
15                          size - index);
16         elementData[index] = element;
17         size++;
18     }

這裡我們只看這兩種新增,另外的兩種addAll思想和這裡的第二種方法類似,就不再做過多的介紹。不難看出二者的區別:

(1)、add(E e) 做的操作第一步先進行快取容量的計算,第二步直接給當前維護的內部快取陣列下一個資料位置填充當前新增的資料。從邏輯結構來看,它是直接在陣列的末尾添加了元素,從物理結構來考慮,是直接在當前資料儲存的實體地址最後面進行開闢了一塊空間進行連續儲存,同時給當前維護陣列大小的變數size++操作。

(2)、add(int index, E element) 操作就有趣的多,它是在當前下標為index的位置新增element,然後將原來index位置的元素進行後移(並不會將原來index位置元素覆蓋掉),先不看原始碼,我們可以想到陣列後移,必定會需要先擴容,然後再進行移動資料。那麼問題來了,如果現在有5000個元素,我要在index = 2 的位置插入一個元素,那要移動4998位,可想而知,在磁碟上進行移動操作,和直接在末尾追加操作哪個效率高就不用我多說了。現在我們看看原始碼時如何實現的,首先去檢查插入的index位是不是越界,然後再計算快取容量,達到快取界限及時擴容(實體地址上的擴容)。然後做一個arraycopy的動作,這個動作用於將當前維護的陣列size變為size+1的大小,同時進行移動資料操作。最後進行index位置賦值位element。

綜上,ArrayList的add操作一定效率低嗎?答案當然是不一定,如果是在ArrayList的末尾新增元素,效率當然高。另外的兩種addAll同樣原始碼會對其先toArray操作,再arraycopy操作。

五、ArrayList的remove()方法

  在這裡,肯定有很多同學疑惑,在工作中寫程式碼時發現寫了一個for迴圈去刪除某個條件的值時,發現會偶爾報錯,有時成功,有時失敗報異常。為何會出現這種情況呢?那我們接下來就看看它的原始碼是如何操作,看完之後或許你會恍然大悟。ArrayList作為容器,肯定會為我們提供容器的基本操作方法。remove就是為我們提供的其中一個。

(1)根據下標刪除指定元素:

 1     /**
 2      * Removes the element at the specified position in this list.
 3      * Shifts any subsequent elements to the left (subtracts one from their
 4      * indices).
 5      *
 6      * @param index the index of the element to be removed
 7      * @return the element that was removed from the list
 8      * @throws IndexOutOfBoundsException {@inheritDoc}
 9      */
10     public E remove(int index) {
11         rangeCheck(index);
12 
13         modCount++;
14         E oldValue = elementData(index);
15 
16         int numMoved = size - index - 1;
17         if (numMoved > 0)
18             System.arraycopy(elementData, index+1, elementData, index,
19                              numMoved);
20         elementData[--size] = null; // clear to let GC do its work
21 
22         return oldValue;
23     }

  我們可以看出,原始碼所做的事情是先獲取到index位置的元素,然後進行arraycopy操作,進行將陣列元素前移(底層使用for迴圈),最關鍵的一點是最後一句

elementData[--size] = null;
 這一行做了一個操作,將陣列的size進行減1操作,這才是為什麼有的同學在使用for迴圈刪除時會發生異常的根本原因。
 例如:ArrayList中的值為[1,3,2,5,2],現在要刪除為2的,第一次找到2時下標為2,remove(2)不會發生異常,然而此時list的大小已經從5變成了4,而迴圈的大小size還是5,導致迴圈下標index為4的時候會發生異常。

(2)同樣,刪除指定元素objec也是同理:

 1     /**
 2      * Removes the first occurrence of the specified element from this list,
 3      * if it is present.  If the list does not contain the element, it is
 4      * unchanged.  More formally, removes the element with the lowest index
 5      * <tt>i</tt> such that
 6      * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
 7      * (if such an element exists).  Returns <tt>true</tt> if this list
 8      * contained the specified element (or equivalently, if this list
 9      * changed as a result of the call).
10      *
11      * @param o element to be removed from this list, if present
12      * @return <tt>true</tt> if this list contained the specified element
13      */
14     public boolean remove(Object o) {
15         if (o == null) {
16             for (int index = 0; index < size; index++)
17                 if (elementData[index] == null) {
18                     fastRemove(index);
19                     return true;
20                 }
21         } else {
22             for (int index = 0; index < size; index++)
23                 if (o.equals(elementData[index])) {
24                     fastRemove(index);
25                     return true;
26                 }
27         }
28         return false;
29     }

  指定刪除元素比直接刪除指定下標更為複雜一點,內部需要先去for迴圈遍歷找到該元素的index,再根據index去刪除,fastRemove(index)方法也是通過arraycopy形式進行移動元素,之後進行size減1操作。原始碼:

 1     /*
 2      * Private remove method that skips bounds checking and does not
 3      * return the value removed.
 4      */
 5     private void fastRemove(int index) {
 6         modCount++;
 7         int numMoved = size - index - 1;
 8         if (numMoved > 0)
 9             System.arraycopy(elementData, index+1, elementData, index,
10                              numMoved);
11         elementData[--size] = null; // clear to let GC do its work
12     }

  因此,ArrayList中的remove方法可能會帶來一些隱患,使用時需要注意,同時效率也是不言而喻的。如果非要使用remove並且還不會發生異常,那我們又該怎麼辦呢?請看下面第五節。

六、ArrayList的Iterator

  ArrayList的內部維護了內部類Iterator,每一個容器都會有自己的迭代器,迭代器作用是為了可以快速的去輪詢ArrayList容器,API建議我們去使用迭代器輪詢元素,而不是使用for迴圈,迭代器為我們提供了規範的介面。

 1     /**
 2      * Returns {@code true} if the iteration has more elements.
 3      * (In other words, returns {@code true} if {@link #next} would
 4      * return an element rather than throwing an exception.)
 5      *
 6      * @return {@code true} if the iteration has more elements
 7      */
 8     boolean hasNext();
 9 
10     /**
11      * Returns the next element in the iteration.
12      *
13      * @return the next element in the iteration
14      * @throws NoSuchElementException if the iteration has no more elements
15      */
16     E next();
17 
18     /**
19      * Removes from the underlying collection the last element returned
20      * by this iterator (optional operation).  This method can be called
21      * only once per call to {@link #next}.  The behavior of an iterator
22      * is unspecified if the underlying collection is modified while the
23      * iteration is in progress in any way other than by calling this
24      * method.
25      *
26      * @implSpec
27      * The default implementation throws an instance of
28      * {@link UnsupportedOperationException} and performs no other action.
29      *
30      * @throws UnsupportedOperationException if the {@code remove}
31      *         operation is not supported by this iterator
32      *
33      * @throws IllegalStateException if the {@code next} method has not
34      *         yet been called, or the {@code remove} method has already
35      *         been called after the last call to the {@code next}
36      *         method
37      */
38     default void remove() {
39         throw new UnsupportedOperationException("remove");
40     }

  ArrayList的內部類去實現了該介面,先看原始碼:

 1  /**
 2      * An optimized version of AbstractList.Itr
 3      */
 4     private class Itr implements Iterator<E> {
 5         int cursor;       // index of next element to return
 6         int lastRet = -1; // index of last element returned; -1 if no such
 7         int expectedModCount = modCount;
 8 
 9         Itr() {}
10 
11         public boolean hasNext() {
12             return cursor != size;
13         }
14 
15         @SuppressWarnings("unchecked")
16         public E next() {
17             checkForComodification();
18             int i = cursor;
19             if (i >= size)
20                 throw new NoSuchElementException();
21             Object[] elementData = ArrayList.this.elementData;
22             if (i >= elementData.length)
23                 throw new ConcurrentModificationException();
24             cursor = i + 1;
25             return (E) elementData[lastRet = i];
26         }
27 
28         public void remove() {
29             if (lastRet < 0)
30                 throw new IllegalStateException();
31             checkForComodification();
32 
33             try {
34                 ArrayList.this.remove(lastRet);
35                 cursor = lastRet;
36                 lastRet = -1;
37                 expectedModCount = modCount;
38             } catch (IndexOutOfBoundsException ex) {
39                 throw new ConcurrentModificationException();
40             }
41         }
42 }

  (1)hasNext()方法,size為ArrayList內容維護陣列實際大小的值,cursor遊標為當前訪問的位置,如果遊標位置剛好指到陣列最後一個位置時則為false,此時停止輪詢遍歷。

  (2)next()方法,遊標從0開始,每次+1,同時記錄最後一次訪問的下標,直接從陣列中獲取該遊標減1(這裡的i)的值。

  (3)remove方法,先呼叫ArrayList容器提供的remove方法,上面已經看過remove方法,此時size會減1,同時此時迭代器的下標已經到了該刪除元素的下一個位置,所以為了防止下標越界,所以將遊標位置前移一位,cursor的下一次訪問從刪除元素位置開始。這樣就保證了刪除時不會發生異常。

七、ArrayList的set()方法

  該方法給某個下標為修改為設定的值,不用過多解釋,直接看原始碼。

 1     /**
 2      * Replaces the element at the specified position in this list with
 3      * the specified element.
 4      *
 5      * @param index index of the element to replace
 6      * @param element element to be stored at the specified position
 7      * @return the element previously at the specified position
 8      * @throws IndexOutOfBoundsException {@inheritDoc}
 9      */
10     public E set(int index, E element) {
11         rangeCheck(index);
12 
13         E oldValue = elementData(index);
14         elementData[index] = element;
15         return oldValue;
16     }

八、ArrayList的get()方法

  這裡也不做過多解釋,直接看原始碼就明白。

 1     /**
 2      * Returns the element at the specified position in this list.
 3      *
 4      * @param  index index of the element to return
 5      * @return the element at the specified position in this list
 6      * @throws IndexOutOfBoundsException {@inheritDoc}
 7      */
 8     public E get(int index) {
 9         rangeCheck(index);
10 
11         return elementData(index);
12     }

九、總結

  ArrayList作為容器,加上前面已經分析過它的原始碼,則可以確定出:

  (1)在ArrayList的尾部插入效率高,隨機訪問效率高(內部是一個數組)。

  (2)但是如果要在中間插入或者刪除則效率低(需要對數組裡面的節點進行位移操作arraycopy非常耗時)。

  (3)使用for迴圈時不要輕易使用remove方法。

  (4)遍歷時推薦使用迭代器,不推薦使用for迴圈。