Java集合幹貨系列-(一)ArrayList源碼解析
前言
今天來介紹下ArrayList,在集合框架整體框架一章中,我們介紹了List接口,ArrayList繼承了AbstractList,實現了List。ArrayList在工作中經常用到,所以要弄懂這個類是極其重要的。
構造圖如下:
藍色線條:繼承
綠色線條:接口實現
正文
ArrayList簡介
ArrayList定義
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList 是一個數組隊列
ArrayList 繼承了AbstractList,實現了List。它是一個數組隊列,提供了相關的添加、刪除、修改、遍歷等功能。
ArrayList 實現了RandmoAccess接口,即提供了隨機訪問功能。RandmoAccess是java中用來被List實現,為List提供快速訪問功能的。在ArrayList中,我們即可以通過元素的序號快速獲取元素對象;這就是快速隨機訪問。稍後,我們會比較List的“快速隨機訪問”和“通過Iterator叠代器訪問”的效率。
ArrayList 實現了Cloneable接口,即覆蓋了函數clone(),能被克隆。
ArrayList 實現java.io.Serializable接口,這意味著ArrayList支持序列化,能通過序列化去傳輸。
和Vector不同,ArrayList中的操作不是線程安全的!所以,建議在單線程中才使用ArrayList,而在多線程中可以選擇Vector或者CopyOnWriteArrayList。
ArrayList屬性
顧名思義哈,ArrayList就是用數組實現的List容器,既然是用數組實現,當然底層用數組來保存數據啦
// 保存ArrayList中數據的數組 private transientObject[] elementData; // ArrayList中實際數據的數量 private int size;
ArrayList包含了兩個重要的對象:elementData 和 size。
(1) elementData 是”Object[]類型的數組”,它保存了添加到ArrayList中的元素。實際上,elementData是個動態數組,我們能通過構造函數 ArrayList(int initialCapacity)來執行它的初始容量為initialCapacity;如果通過不含參數的構造函數ArrayList()來創建ArrayList,則elementData的容量默認是10。elementData數組的大小會根據ArrayList容量的增長而動態的增長,具體的增長方式,請參考源碼分析中的ensureCapacity()函數。
(2) size 則是動態數組的實際大小。
ArrayList構造函數
// ArrayList帶容量大小的構造函數。 public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity); // 新建一個數組 this.elementData = new Object[initialCapacity]; } // ArrayList構造函數。默認容量是10。 public ArrayList() { this(10); } // 構造一個包含指定元素的list,這些元素的是按照Collection的叠代器返回的順序排列的 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); size = elementData.length; if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); }17:41:58
- 第一個構造方法使用提供的initialCapacity來初始化elementData數組的大小。
- 第二個構造方法調用第一個構造方法並傳入參數10,即默認elementData數組的大小為10。
- 第三個構造方法則將提供的集合轉成數組返回給elementData(返回若不是Object[]將調用Arrays.copyOf方法將其轉為Object[])。
API方法摘要
ArrayList源碼解析(基於JDK1.6.0_45)
增加
/** * 添加一個元素 */ public boolean add(E e) { // 進行擴容檢查 ensureCapacity( size + 1); // Increments modCount // 將e增加至list的數據尾部,容量+1 elementData[size ++] = e; return true; } /** * 在指定位置添加一個元素 */ public void add(int index, E element) { // 判斷索引是否越界,這裏會拋出多麽熟悉的異常。。。 if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: "+index+", Size: " +size); // 進行擴容檢查 ensureCapacity( size+1); // Increments modCount // 對數組進行復制處理,目的就是空出index的位置插入element,並將index後的元素位移一個位置 System. arraycopy(elementData, index, elementData, index + 1, size - index); // 將指定的index位置賦值為element elementData[index] = element; // list容量+1 size++; } /** * 增加一個集合元素 */ public boolean addAll(Collection<? extends E> c) { //將c轉換為數組 Object[] a = c.toArray(); int numNew = a.length ; //擴容檢查 ensureCapacity( size + numNew); // Increments modCount //將c添加至list的數據尾部 System. arraycopy(a, 0, elementData, size, numNew); //更新當前容器大小 size += numNew; return numNew != 0; } /** * 在指定位置,增加一個集合元素 */ public boolean addAll(int index, Collection<? extends E> c) { if (index > size || index < 0) throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size); Object[] a = c.toArray(); int numNew = a.length ; ensureCapacity( size + numNew); // Increments modCount // 計算需要移動的長度(index之後的元素個數) int numMoved = size - index; // 數組復制,空出第index到index+numNum的位置,即將數組index後的元素向右移動numNum個位置 if (numMoved > 0) System. arraycopy(elementData, index, elementData, index + numNew, numMoved); // 將要插入的集合元素復制到數組空出的位置中 System. arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; } /** * 數組容量檢查,不夠時則進行擴容 */ public void ensureCapacity( int minCapacity) { modCount++; // 當前數組的長度 int oldCapacity = elementData .length; // 最小需要的容量大於當前數組的長度則進行擴容 if (minCapacity > oldCapacity) { Object oldData[] = elementData; // 新擴容的數組長度為舊容量的1.5倍+1 int newCapacity = (oldCapacity * 3)/2 + 1; // 如果新擴容的數組長度還是比最小需要的容量小,則以最小需要的容量為長度進行擴容 if (newCapacity < minCapacity) newCapacity = minCapacity; // minCapacity is usually close to size, so this is a win: // 進行數據拷貝,Arrays.copyOf底層實現是System.arrayCopy() elementData = Arrays.copyOf( elementData, newCapacity); } }
刪除
/** * 根據索引位置刪除元素 */ public E remove( int index) { // 數組越界檢查 RangeCheck(index); modCount++; // 取出要刪除位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 計算數組要復制的數量 int numMoved = size - index - 1; // 數組復制,就是將index之後的元素往前移動一個位置 if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); // 將數組最後一個元素置空(因為刪除了一個元素,然後index後面的元素都向前移動了,所以最後一個就沒用了),好讓gc盡快回收 // 不要忘了size減一 elementData[--size ] = null; // Let gc do its work return oldValue; } /** * 根據元素內容刪除,只刪除匹配的第一個 */ public boolean remove(Object o) { // 對要刪除的元素進行null判斷 // 對數據元素進行遍歷查找,知道找到第一個要刪除的元素,刪除後進行返回,如果要刪除的元素正好是最後一個那就慘了,時間復雜度可達O(n) 。。。 if (o == null) { for (int index = 0; index < size; index++) // null值要用==比較 if (elementData [index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) // 非null當然是用equals比較了 if (o.equals(elementData [index])) { fastRemove(index); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(int index) { modCount++; // 原理和之前的add一樣,還是進行數組復制,將index後的元素向前移動一個位置,不細解釋了, int numMoved = size - index - 1; if (numMoved > 0) System. arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size ] = null; // Let gc do its work } /** * 數組越界檢查 */ private void RangeCheck(int index) { if (index >= size ) throw new IndexOutOfBoundsException( "Index: "+index+", Size: " +size); }
增加和刪除方法到這裏就解釋完了,代碼是很簡單,主要需要特別關心的就兩個地方:1.數組擴容,2.數組復制,這兩個操作都是極費效率的,最慘的情況下(添加到list第一個位置,刪除list最後一個元素或刪除list第一個索引位置的元素)時間復雜度可達O(n)。
還記得上面那個坑嗎(為什麽提供一個可以指定容量大小的構造方法 )?看到這裏是不是有點明白了呢,簡單解釋下:如果數組初試容量過小,假設默認的10個大小,而我們使用ArrayList的主要操作時增加元素,不斷的增加,一直增加,不停的增加,會出現上面後果?那就是數組容量不斷的受挑釁,數組需要不斷的進行擴容,擴容的過程就是數組拷貝System.arraycopy的過程,每一次擴容就會開辟一塊新的內存空間和數據的復制移動,這樣勢必對性能造成影響。那麽在這種以寫為主(寫會擴容,刪不會縮容)場景下,提前預知性的設置一個大容量,便可減少擴容的次數,提高了性能。
上面兩張圖分別是數組擴容和數組復制的過程,需要註意的是,數組擴容伴隨著開辟新建的內存空間以創建新數組然後進行數據復制,而數組復制不需要開辟新內存空間,只需將數據進行復制。
上面講增加元素可能會進行擴容,而刪除元素卻不會進行縮容,如果在已刪除為主的場景下使用list,一直不停的刪除而很少進行增加,那麽會出現什麽情況?再或者數組進行一次大擴容後,我們後續只使用了幾個空間,會出現上面情況?當然是空間浪費啦啦啦,怎麽辦呢?
/** * 將底層數組的容量調整為當前實際元素的大小,來釋放空間。 */ public void trimToSize() { modCount++; // 當前數組的容量 int oldCapacity = elementData .length; // 如果當前實際元素大小 小於 當前數組的容量,則進行縮容 if (size < oldCapacity) { elementData = Arrays.copyOf( elementData, size ); }
更新
/** * 將指定位置的元素更新為新元素 */ public E set( int index, E element) { // 數組越界檢查 RangeCheck(index); // 取出要更新位置的元素,供返回使用 E oldValue = (E) elementData[index]; // 將該位置賦值為行的元素 elementData[index] = element; // 返回舊元素 return oldValue; }
查找
/** * 查找指定位置上的元素 */ public E get( int index) { RangeCheck(index); return (E) elementData [index]; }
是否包含
/** * Returns <tt>true</tt> if this list contains the specified element. * More formally, returns <tt>true</tt> if and only if this list contains * at least one element <tt>e</tt> such that * <tt>(o==null ? e==null : o.equals(e))</tt>. * * @param o element whose presence in this list is to be tested * @return <tt> true</tt> if this list contains the specified element */ public boolean contains(Object o) { return indexOf(o) >= 0; } /** * Returns the index of the first occurrence of the specified element * in this list, or -1 if this list does not contain the element. * More formally, returns the lowest index <tt>i</tt> such that * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>, * or -1 if there is no such index. */ public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData [i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData [i])) return i; } return -1; } /** * Returns the index of the last occurrence of the specified element * in this list, or -1 if this list does not contain the element. * More formally, returns the highest index <tt>i</tt> such that * <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>, * or -1 if there is no such index. */ public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData [i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData [i])) return i; } return -1; }
contains主要是檢查indexOf,也就是元素在list中出現的索引位置也就是數組下標,再看indexOf和lastIndexOf代碼是不是很熟悉,沒錯,和public boolean remove(Object o) 的代碼一樣,都是元素null判斷,都是循環比較,不多說了。。。但是要知道,最差的情況(要找的元素是最後一個)也是很慘的。。。
容量判斷
/** * Returns the number of elements in this list. * * @return the number of elements in this list */ public int size() { return size ; } /** * Returns <tt>true</tt> if this list contains no elements. * * @return <tt> true</tt> if this list contains no elements */ public boolean isEmpty() { return size == 0; }
由於使用了size進行計數,發現list大小獲取和判斷真的好容易。
總結:
(01) ArrayList 實際上是通過一個數組去保存數據的。當我們構造ArrayList時;若使用默認構造函數,則ArrayList的默認容量大小是10。
(02) 當ArrayList容量不足以容納全部元素時,ArrayList會重新設置容量:新的容量=“(原始容量x3)/2 + 1”。
(03) ArrayList的克隆函數,即是將全部元素克隆到一個數組中。
(04) ArrayList實現java.io.Serializable的方式。當寫入到輸出流時,先寫入“容量”,再依次寫入“每一個元素”;當讀出輸入流時,先讀取“容量”,再依次讀取“每一個元素”。
ArrayList遍歷方式
ArrayList支持3種遍歷方式
(01) 第一種,通過叠代器遍歷。即通過Iterator去遍歷。
Integer value = null; Iterator iter = list.iterator(); while (iter.hasNext()) { value = (Integer)iter.next(); }
(02) 第二種,隨機訪問,通過索引值去遍歷。
由於ArrayList實現了RandomAccess接口,它支持通過索引值去隨機訪問元素。
Integer value = null; int size = list.size(); for (int i=0; i<size; i++) { value = (Integer)list.get(i); }
(03) 第三種,for循環遍歷。如下:
Integer value = null; for (Integer integ:list) { value = integ; }
下面通過一個實例,比較這3種方式的效率,實例代碼(ArrayListRandomAccessTest.java)如下:
import java.util.*; import java.util.concurrent.*; /* * @desc ArrayList遍歷方式和效率的測試程序。 * * @author skywang */ public class ArrayListRandomAccessTest { public static void main(String[] args) { List list = new ArrayList(); for (int i=0; i<100000; i++) list.add(i); //isRandomAccessSupported(list); iteratorThroughRandomAccess(list) ; iteratorThroughIterator(list) ; iteratorThroughFor2(list) ; } private static void isRandomAccessSupported(List list) { if (list instanceof RandomAccess) { System.out.println("RandomAccess implemented!"); } else { System.out.println("RandomAccess not implemented!"); } } public static void iteratorThroughRandomAccess(List list) { long startTime; long endTime; startTime = System.currentTimeMillis(); for (int i=0; i<list.size(); i++) { list.get(i); } endTime = System.currentTimeMillis(); long interval = endTime - startTime; System.out.println("iteratorThroughRandomAccess:" + interval+" ms"); } public static void iteratorThroughIterator(List list) { long startTime; long endTime; startTime = System.currentTimeMillis(); for(Iterator iter = list.iterator(); iter.hasNext(); ) { iter.next(); } endTime = System.currentTimeMillis(); long interval = endTime - startTime; System.out.println("iteratorThroughIterator:" + interval+" ms"); } public static void iteratorThroughFor2(List list) { long startTime; long endTime; startTime = System.currentTimeMillis(); for(Object obj:list) ; endTime = System.currentTimeMillis(); long interval = endTime - startTime; System.out.println("iteratorThroughFor2:" + interval+" ms"); } }
運行結果:
iteratorThroughRandomAccess:3 ms
iteratorThroughIterator:8 ms
iteratorThroughFor2:5 ms
由此可見,遍歷ArrayList時,使用隨機訪問(即,通過索引序號訪問)效率最高,而使用叠代器的效率最低!
ArrayList示例
本文通過一個實例(ArrayListTest.java),介紹 ArrayList 中常用API的用法。
import java.util.*; /* * @desc ArrayList常用API的測試程序 * @author skywang * @email [email protected] */ public class ArrayListTest { public static void main(String[] args) { // 創建ArrayList ArrayList list = new ArrayList(); // 將“” list.add("1"); list.add("2"); list.add("3"); list.add("4"); // 將下面的元素添加到第1個位置 list.add(0, "5"); // 獲取第1個元素 System.out.println("the first element is: "+ list.get(0)); // 刪除“3” list.remove("3"); // 獲取ArrayList的大小 System.out.println("Arraylist size=: "+ list.size()); // 判斷list中是否包含"3" System.out.println("ArrayList contains 3 is: "+ list.contains(3)); // 設置第2個元素為10 list.set(1, "10"); // 通過Iterator遍歷ArrayList for(Iterator iter = list.iterator(); iter.hasNext(); ) { System.out.println("next is: "+ iter.next()); } // 將ArrayList轉換為數組 String[] arr = (String[])list.toArray(new String[0]); for (String str:arr) System.out.println("str: "+ str); // 清空ArrayList list.clear(); // 判斷ArrayList是否為空 System.out.println("ArrayList is empty: "+ list.isEmpty()); } }
運行結果:
the first element is: 5 Arraylist size=: 4 ArrayList contains 3 is: false next is: 5 next is: 10 next is: 2 next is: 4 str: 5 str: 10 str: 2 str: 4 ArrayList is empty: true
總結
ArrayList和LinkedList的區別
- ArrayList是實現了基於動態數組的數據結構,LinkedList基於鏈表的數據結構。
- 對於隨機訪問get和set,ArrayList覺得優於LinkedList,因為LinkedList要移動指針。
- 對於新增和刪除操作add和remove,LinkedList比較占優勢,因為ArrayList要移動數據。
ArrayList和Vector的區別
- Vector和ArrayList幾乎是完全相同的,唯一的區別在於Vector是同步類(synchronized),屬於強同步類。因此開銷就比ArrayList要大,訪問要慢。正常情況下,大多數的Java程序員使用ArrayList而不是Vector,因為同步完全可以由程序員自己來控制。
- Vector每次擴容請求其大小的2倍空間,而ArrayList是1.5倍。
- Vector還有一個子類Stack.
參考
該文為本人學習的筆記,參考網上各大帖子,取其精華整合自己的理解而成。集合框架源碼面試經常會問,所以解讀源碼十分必要,希望對你有用。
Java集合框架:ArrayList
Java 集合系列03之 ArrayList詳細介紹(源碼解析)和使用示例
給jdk寫註釋系列之jdk1.6容器(1)-ArrayList源碼解析
java容器類源碼分析——ArrayList
整理的集合框架思維導圖
個人整理的Java集合框架思維導圖,動態維護。導出的圖片無法查看備註的一些信息,所以需要源文件的童鞋可以關註我個人主頁上的公眾號,回復Java集合框架即可獲取源文件。
原文鏈接:http://tengj.top/2016/04/13/javajh1arraylist/#
Java集合幹貨系列-(一)ArrayList源碼解析