Java基礎系列--ArrayList集合

原創做品,能夠轉載,可是請標註出處地址:http://www.cnblogs.com/sky-jie/p/8617593.htmlhtml

1、概述java

  ArrayList是Java集合體系中最常使用,也是最簡單的集合類,是以數組實現的線性表。算法

  數組在內存中是以一段連續的內存來進行存放的,一樣,ArrayList也是如此,初始化時能夠指定初始容量,也能夠以默認容量(10)建立底層數組,因爲ArrayList屬於可變長列表,採用可變數組實現,數組自己是不變的,一旦定義就沒法變長,可變數組使用建立新數組拷貝舊數據的方式間接實現可變長,習慣稱爲擴容。數組

  ArrayList底層數組的擴容算法依據的是一個擴容算法來計算新的數組長度,擴容的條件是當前底層數組不足以容納新的元素。ide

2、繼承結構 優化

  ArrayList的類結構以下所示:this

3、底層實現spa

3.1 初始化線程

  如前所述,ArrayList底層採用的是數組結構。3d

1     private transient Object[] elementData;

  elementData就是定義在ArrayList底層的數組,而數組就是一連串連續的內存,其邏輯結構以下:

  上圖表示初始容量爲10的ArrayList的底層數組,默認的初始容量爲10,而列表長度用size來定義:

1     private int size;

  這個表示的是列表中元素的數量,與數組的長度不一樣,size默認爲0,表示列表爲空。

  1.7版本:

  ArrayList的初始化由兩個基本的構造器實現:

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

  第一個構造器用於定義一個默認的ArrayList,這個列表擁有默認的10長度的底層數組,第二個構造器用於自行定製指定初始容量的ArrayList。其中第一個構造器調用第二個構造器來實現的。

  在第二個構造器中,只進行了容量值不爲負值的簡單校驗。

  1.8版本:

  1.8版本中對這部分進行了優化,在1.8中定義了兩個空數組:

1     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 2 private static final Object[] EMPTY_ELEMENTDATA = {}; 3 private static final int DEFAULT_CAPACITY = 10;

  其中:EMPTY_ELEMENTDATA表明一個簡單的0容量的空數組,用於表示一個空列表,DEFAULTCAPACITY_EMPTY_ELEMENTDATA一樣表示一個空列表,可是它含有一層區別意味,意指默認容量的空列表(雖然它自己任然是0容量的)。

  因爲這些因素,其初始化邏輯也作了優化:

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

  第一個構造器構建的是一個默認容量的列表,但實質上只是一個容量爲0的空數組(空列表),它的容量指定是在第一個元素被添加進來以前指定的,容量爲10(默認容量),有效的節省了內存空間,這個列表在不使用以前,幾乎不佔任何堆內存。

  第二個構造器,在1.7版本的基礎上加了一個initialCapacity==0的判斷狀況,這種狀況將列表定義爲一個容量爲0 的空數組EMPTY_ELEMENTDATA。爲何不直接使用new建立呢?由於定義爲指定的EMPTY_ELEMENTDATA能夠極大的節省內存空間,由於EMPTY_ELEMENTDATA是私有靜態不變的。

3.2 添加元素

  初始化的ArrayList的底層數組是沒有元素的,即數組的各位均爲null(在1.8版本中底層數組是0長度的數組)。使用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。

  參照下方實例,原始列表有兩個元素:e一、e2,如今給定集合包含3個元素,e三、e四、e5,如今執行add(1,Collection<? extends E>)

  也就是將給定集合的元素嵌入到當前列表中。

  總結:咱們能夠看到在添加元素以前,咱們都須要對列表的容量進行校驗,以肯定已有的空餘容量可否容納新添加的元素,若是檢查發現容量不足,就必須進行擴容,見3.7。

 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.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 }

  1.8版本:

 1     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;  2 private void ensureCapacityInternal(int minCapacity) {  3  ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));  4  }  5  6 private static int calculateCapacity(Object[] elementData, int minCapacity) {  7 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {  8 return Math.max(DEFAULT_CAPACITY, minCapacity);  9  } 10 return minCapacity; 11  } 12 13 private void ensureExplicitCapacity(int minCapacity) { 14 modCount++; 15 16 // overflow-conscious code 17 if (minCapacity - elementData.length > 0) 18  grow(minCapacity); 19  } 20 21 private void grow(int minCapacity) { 22 // overflow-conscious code 23 int oldCapacity = elementData.length; 24 int newCapacity = oldCapacity + (oldCapacity >> 1); 25 if (newCapacity - minCapacity < 0) 26 newCapacity = minCapacity; 27 if (newCapacity - MAX_ARRAY_SIZE > 0) 28 newCapacity = hugeCapacity(minCapacity); 29 // minCapacity is usually close to size, so this is a win: 30 elementData = Arrays.copyOf(elementData, newCapacity); 31  } 32 33 private static int hugeCapacity(int minCapacity) { 34 if (minCapacity < 0) // overflow 35 throw new OutOfMemoryError(); 36 return (minCapacity > MAX_ARRAY_SIZE) ? 37  Integer.MAX_VALUE : 38  MAX_ARRAY_SIZE; 39 }

  可見1.8版本對容量校驗的部分進行了再封裝,這一部分主要就是以前3.1中講述的關於首次添加元素時進行容量指定的內容,重點在calculateCapacity方法中,指定要校驗的容量minCapacity和初始容量做比較,若是初始容量大則使用初始容量,不然使用指定容量,這些也只會在首次添加元素時進行,以後就不會存在初始容量大的狀況了。

  擴容條件:拿給定的容量(長度值)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。兩者雖然都是迭代器,可是仍是有些不一樣的。

  Iterator迭代器擁有前序遍歷列表的功能,和刪除元素的功能,這些刪除會實時體如今列表中。

  ListIterator迭代器是Iterator的子類,擁有Iterator的所有功能,而且進行了簡單擴展,新增了修改元素和添加新元素的功能,一樣會實時提現到列表中。

  這些擴展的功能通常都是依附遍歷而存在的,不能脫離遍歷使用。好比remove操做,直接進行remove是會報錯的,由於迭代器的指針指向的是首個元素以前的位置,這是刪除是沒法進行刪除的,只有執行了next()方法,指針跳過若干元素以後,remove操做會將剛跳過的元素刪除。

  add操做的做用是在指針剛剛跳過的元素後面與下一個元素之間插入新元素,修改元素則是針對指針剛剛跳過的元素進行修改。

 

 1 import java.util.ArrayList;  2 import java.util.ListIterator;  3  4 public class ListTest {  5 public static void main(String[] args){  6 ArrayList<String> list = new ArrayList<>();  7 list.add("1");list.add("2");list.add("3");list.add("4");list.add("5");list.add("6");  8 System.out.println("初始列表爲:"+list);  9 ListIterator<String> lit = list.listIterator(); 10 lit.add("10"); 11 System.out.println("迭代器指針移動前直接添加元素列表爲:"+list); 12 String s = lit.next(); 13 lit.add("100"); 14 System.out.println("迭代器指針移動後再次添加元素後:"+list); 15 s = lit.next(); 16 lit.set("200"); 17 System.out.println("修改元素:"+list); 18 s = lit.next(); 19  lit.remove(); 20 System.out.println("刪除元素爲:"+list); 21  } 22 }

 

  執行結果爲:

1 初始列表爲:[1, 2, 3, 4, 5, 6]
2 迭代器指針移動前直接添加元素列表爲:[10, 1, 2, 3, 4, 5, 6]
3 迭代器指針移動後再次添加元素後:[10, 1, 100, 2, 3, 4, 5, 6]
4 修改元素:[10, 1, 100, 200, 3, 4, 5, 6]
5 刪除元素爲:[10, 1, 100, 200, 4, 5, 6]
View Code

3.9 Spliterator迭代器

  Spliterator是1.8新增的迭代器,屬於並行迭代器,能夠將迭代任務分割交由多個線程來進行。詳情之後單立一篇進行講述。

相關文章
相關標籤/搜索