Java基礎系列--集合之ArrayList
原創作品,可以轉載,但是請標註出處地址:http://www.cnblogs.com/V1haoge/p/8494618.html
一、概述
ArrayList是Java集合體系中最常使用,也是最簡單的集合類,是以數組實現的線性表。
數組在內存中是以一段連續的內存來進行存放的,同樣,ArrayList也是如此,初始化時可以指定初始容量,也可以以默認容量(10)創建底層數組,由於ArrayList屬於可變長列表,采用可變數組實現,數組本身是不變的,一旦定義就無法變長,可變數組使用創建新數組拷貝舊數據的方式間接實現可變長,習慣稱為擴容。
ArrayList底層數組的擴容算法依據的是一個擴容算法來計算新的數組長度,擴容的條件是當前底層數組不足以容納新的元素。
二、繼承結構
ArrayList的類結構如下所示:
三、底層實現
3.1 底層結構
如前所述,ArrayList底層采用的是數組結構。
1 private transient Object[] elementData;
elementData就是定義在ArrayList底層的數組,而數組就是一連串連續的內存,其邏輯結構如下:
上圖表示初始容量為10的ArrayList的底層數組,默認的初始容量為10,而列表長度用size來定義:
1 private int size;
這個表示的是列表中元素的數量,與數組的長度不同,size默認為0,表示列表為空。
3.2 添加元素
初始化的ArrayList的底層數組是沒有元素的,即數組的各位均為null。使用add方法我們可以為列表添加元素,ArrayList中的添加單個元素有兩種方式,一種是直接添加,另一種是定位添加。還有添加一組元素的兩種方法,一種是定組直接添加,一種是定位定組添加
3.2.1 直接添加
所謂直接添加就是將新元素添加到列表末尾,其實現邏輯如下:
1 public boolean add(E e) { 2 ensureCapacityInternal(size + 1); // Increments modCount!! 3 elementData[size++] = e;4 return true; 5 }
上面的邏輯很簡單,首先進行列表容量檢測(容後詳述),然後直接將新元素放置到底層數組中即可。
圖中顯示添加新元素到列表中,添加之後size的值會增加,這個size即指向數組最新空位的下標,有代表數組中元素的個數。
3.2.2 定位添加
所謂定位添加,就是我們將新元素,添加到列表指定下標處。
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 }
首先進行index參數校驗,校驗通過後進行列表容量檢測(容後詳述),然後將指定下標處開始到結尾的所以元素整體後移以為,下標處空出來後填充新添加的元素。這個添加操作涉及到一部分元素的整體移動,較為耗時,具體視實際移動的元素數量而定。
實例:原始列表中由e1-e5共5個元素,現在執行add(2,e6),表示在下標2處添加元素e6,執行步驟如下:
註意:add(int,E)方法底層數組元素的後移操作采用的是System.arraycopy()方法實現的,不僅此處,後面還會多次使用這個方法來實現數組元素的拷貝。
3.2.3 定組直接添加
定組直接添加方法為:addAll(Collection<? extends E>),直接將給定的集合中的元素依次添加到當前列表的後面。
1 public boolean addAll(Collection<? extends E> c) { 2 Object[] a = c.toArray(); 3 int numNew = a.length; 4 ensureCapacityInternal(size + numNew); // Increments modCount 5 System.arraycopy(a, 0, elementData, size, numNew); 6 size += numNew; 7 return numNew != 0; 8 }
首先進行集合轉化,將其轉化為數組,獲取其長度(元素個數),進行列表容量檢測(容後詳述),將轉化好的數組元素復制到當前列表的底層數組後面,計算size。
明顯類似於直接添加,只是添加的數量不同而已,做個簡單的圖示:
只是這裏講給定的集合簡化為數組形式,其實在源碼中我們也能發現,在第2行對集合進行了數組轉化,便於操作。元素的添加還是使用數組拷貝的形式實現。
3.2.4 定位定組添加
定位定組添加類似於定位添加,同樣只是添加的元素個數不同。
1 public boolean addAll(int index, Collection<? extends E> c) { 2 rangeCheckForAdd(index); 3 4 Object[] a = c.toArray(); 5 int numNew = a.length; 6 ensureCapacityInternal(size + numNew); // Increments modCount 7 8 int numMoved = size - index; 9 if (numMoved > 0) 10 System.arraycopy(elementData, index, elementData, index + numNew, 11 numMoved); 12 13 System.arraycopy(a, 0, elementData, index, numNew); 14 size += numNew; 15 return numNew != 0; 16 }
首先進行index參數校驗,然後將集合轉化為數組,獲取其中元素個數numNew,再進行列表容量檢測,獲取需要後移的元素的個數,使用數組復制的方式將這些元素後移,再將轉化的數組元素復制遷移到空出的空位處。計算size。
參照下方實例,原始列表有兩個元素:e1、e2,現在給定集合包含3個元素,e3、e4、e5,現在執行add(1,Collection<? extends E>)
也就是將給定集合的元素嵌入到當前列表中。
3.3 獲取元素
獲取指定下標處的元素,復雜度O(1),只要獲取數組執行下標處的元素就可以。
1 public E get(int index) { 2 rangeCheck(index); 3 4 return elementData(index); 5 }
首先進行index參數校驗,然後獲取返回數組執行下標的元素。
3.4 修改元素
修改元素需要提供修改下標和新值,直接用新值替換舊值即可。
1 public E set(int index, E element) { 2 rangeCheck(index); 3 4 E oldValue = elementData(index); 5 elementData[index] = element; 6 return oldValue; 7 }
首先進行index參數校驗,然後保存指定下標的舊值,替換為新值,將舊值返回。
3.5 刪除元素
刪除元素的操作挺多的,針對不同的情況:
3.5.1 定位刪除
定位刪除,即刪除指定下標的元素,需要提供刪除的元素的下標。
1 public E remove(int index) { 2 rangeCheck(index); 3 4 modCount++; 5 E oldValue = elementData(index); 6 7 int numMoved = size - index - 1; 8 if (numMoved > 0) 9 System.arraycopy(elementData, index+1, elementData, index, 10 numMoved); 11 elementData[--size] = null; // Let gc do its work 12 13 return oldValue; 14 }
首先進行index參數校驗,modCount自增1,保存指定下標處的舊值,然後將指定下標的下一個元素到結尾的元素整體前移一位,後面元素覆蓋前面元素,指定下標處的舊值被刪除,然後將原來的末尾元素置空,size自減1,最後將舊值返回。
同樣數組元素的移動采用數組復制的方式實現。
實例:原始列表擁有5個元素,e1-e5,現移除下標2處的元素:remove(2)
3.5.2 定元素刪除
指定要刪除的元素的值,在列表中查詢該值,刪除查詢到的第一個。即該方法只會刪除符合條件的首個元素(即使列表中存在多個符合條件的元素)。
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(index); 6 return true; 7 } 8 } else { 9 for (int index = 0; index < size; index++) 10 if (o.equals(elementData[index])) { 11 fastRemove(index); 12 return true; 13 } 14 } 15 return false; 16 } 17 18 private void fastRemove(int index) { 19 modCount++; 20 int numMoved = size - index - 1; 21 if (numMoved > 0) 22 System.arraycopy(elementData, index+1, elementData, index, 23 numMoved); 24 elementData[--size] = null; // Let gc do its work 25 }
指定元素進行刪除的情況,較為復雜,需要針對元素的情況進行分析,如果指定元素為null,則刪除第一個null元素,若指定元素非null,則查詢首個符合條件的元素進行刪除。
欲刪除元素,必須先找到要刪除元素的下標,這個過程由循環實現(第3行和第9行),找到下標之後,調用內部方法fastRemove(int),進行指定下標元素的刪除,即定位刪除,然後返回true,表示刪除成功。
還有一種情況那就是指定的元素在列表中查詢不到,這是直接返回false即可。
這種刪除底層使用的仍然是定位刪除。不在畫圖舉例了。
3.5.3 定組刪除
所謂定組刪除,就是刪除當前列表中所以與給定集合中元素相同的元素,該操作需要制定一個欲要刪除的元素的集合(Collection)。
1 public boolean removeAll(Collection<?> c) { 2 return batchRemove(c, false); 3 } 4 5 private boolean batchRemove(Collection<?> c, boolean complement) { 6 final Object[] elementData = this.elementData; 7 int r = 0, w = 0; 8 boolean modified = false; 9 try { 10 for (; r < size; r++) 11 if (c.contains(elementData[r]) == complement) 12 elementData[w++] = elementData[r]; 13 } finally { 14 // Preserve behavioral compatibility with AbstractCollection, 15 // even if c.contains() throws. 16 if (r != size) { 17 System.arraycopy(elementData, r, 18 elementData, w, 19 size - r); 20 w += size - r; 21 } 22 if (w != size) { 23 for (int i = w; i < size; i++) 24 elementData[i] = null; 25 modCount += size - w; 26 size = w; 27 modified = true; 28 } 29 } 30 return modified; 31 }
定組刪除的complement傳值為false,用於第11行比較,表示如果給定集合中不包含當前列表的當前下標的元素的情況下,執行內部語句塊,將這個元素保留下來(亦即若包含該元素則不保留該元素,最後查遺補漏時,會將其消失【finally塊邏輯】)
實例:當前列表是包含5個元素e1-e5,給定集合元素包括e2,e3,e5三個元素,則定位刪除的圖示為:
從第二步開始循環,每次循環結束,r就會自增1,而w表示的是下一個保留元素的下標或者保留元素的個數。循環在第七步r自增到5,不滿足循環條件時結束,最後r=5,w=2,亦即共刪除3個元素,modCount需要自增(r-w=5-2=3)次。
最後finally中執行第二個if塊,將多余的元素位置置null,再計算modCount和size。
3.5.4 定組保留刪除
定組保留刪除邏輯與定組刪除正好相反,需要將給定集合中包含的當前列表的元素保留下來,將不包含的刪除。
1 public boolean retainAll(Collection<?> c) { 2 return batchRemove(c, true); 3 }
實例,同上,圖示如下:
執行過程同上。
3.5.5 範圍刪除
ArrayList中還有一個範圍刪除方法:removeRange(int,int),根據給定的兩個下標,刪除下標範圍之內的所有元素。該方法是protected修飾的,那麽很明顯,這個方法並不是面向ArrayList使用者的,而是面向JDK實現者的,這裏只做簡單介紹。
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 }
很簡單,將toIndex到結尾的元素復制到fromIndex,空出的位置全部置空即可。
3.6 查找元素
查找元素包括3個方法:
contains(Object) 檢查當前列表是否包含某個元素
indexOf(Object) 檢查給定元素在當前列表中首次出現的下標
lastIndexOf(Object) 檢查給定元素在當前列表中最後出現的下標
3.6.1 包含方法
1 public boolean contains(Object o) { 2 return indexOf(o) >= 0; 3 }
好家夥,自己啥也不幹,就靠後面了...
3.6.2 前序查找
前序查找就是從頭開始查找元素,返回首次出現的下標。
1 public int indexOf(Object o) { 2 if (o == null) { 3 for (int i = 0; i < size; i++) 4 if (elementData[i]==null) 5 return i; 6 } else { 7 for (int i = 0; i < size; i++) 8 if (o.equals(elementData[i])) 9 return i; 10 } 11 return -1; 12 }
源碼顯示,需要分兩種情況考慮,如果給定元素為null,則查找首個null元素的下標並返回,如果給定元素非null,則查找首次出現的下標並返回,如果列表中不包含該元素,則返回-1。
3.6.3 後序查找
後序查找就是前序查找的反向查找方式:
1 public int lastIndexOf(Object o) { 2 if (o == null) { 3 for (int i = size-1; i >= 0; i--) 4 if (elementData[i]==null) 5 return i; 6 } else { 7 for (int i = size-1; i >= 0; i--) 8 if (o.equals(elementData[i])) 9 return i; 10 } 11 return -1; 12 }
參考前後查找的源碼不難發現,模式一致,只是查找的方向不同而已。
3.7 列表擴容
列表的擴容是底層自動進行的,對列表的使用者是完全透明的,因此其方法都是私有的。擴容的條件與算法並不復雜:
1 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 2 3 private void ensureCapacityInternal(int minCapacity) { 4 modCount++; 5 // overflow-conscious code 6 if (minCapacity - elementData.length > 0) 7 grow(minCapacity); 8 } 9 10 private void grow(int minCapacity) { 11 // overflow-conscious code 12 int oldCapacity = elementData.length; 13 int newCapacity = oldCapacity + (oldCapacity >> 1); 14 if (newCapacity - minCapacity < 0) 15 newCapacity = minCapacity; 16 if (newCapacity - MAX_ARRAY_SIZE > 0) 17 newCapacity = hugeCapacity(minCapacity); 18 // minCapacity is usually close to size, so this is a win: 19 elementData = Arrays.copyOf(elementData, newCapacity); 20 } 21 22 private static int hugeCapacity(int minCapacity) { 23 if (minCapacity < 0) // overflow 24 throw new OutOfMemoryError(); 25 return (minCapacity > MAX_ARRAY_SIZE) ? 26 Integer.MAX_VALUE : 27 MAX_ARRAY_SIZE; 28 }
擴容條件:拿給定的容量(長度值)minCapacity與當前列表的底層數組的容量elementData.length進行比較,如果前者大,則說明容量不足,需要擴容,調用grow(minCapacity)進行擴容。
擴容算法:擴容時存在三種情況,第一種就是普通的自動擴容,按照oldCapacity + (oldCapacity >> 1)算法進行擴容,上式計算得出的即為新的數組容量,一般針對的是單個元素添加的情況,即直接添加和定位添加的情況,這種情況下,每次只添加一個元素,不會出現第14行成功的可能,但是如果是定組直接添加和定位定組添加的時候,由於添加的集合元素數量未知,一旦給定的minCapacity比計算得出的新容量要大,說明計算得出的容量不足以容納所有的元素,這是直接使用minCapacity作為新容量進行擴容。即優先使用算法計算的容量進行擴容,一旦計算容量還不足以容納新元素,則使用給定的容量進行擴容。
還有一種特殊情況,當本次擴容時,計算得到的容量,或者給定的容量大於MAX_ARRAY_SIZE(=Integer.MAX_VALUE - 8)的情況下,需要調用hugeCapacity(minCapacity)方法進行人為限制容量超限,將容量限制在整形的最大值之內。
最後進行數組擴容,創建新數組,拷貝數據。
3.8 叠代器
列表的叠代必不可少,而且這裏還會用到一個出現很久的變量modCount,此前我們對它一無所知。
modCount記錄的是列表結構發生變化的次數,所謂結構變化包括:新增元素,刪除元素,清空元素,擴容等。
ArrayList的叠代器有兩種,ListIterator和Iterator。二者雖然都是叠代器,但是還是有些不同的。
(待續)
Java基礎系列--集合之ArrayList