知識點:Java 集合框架圖
總結:Java 集合進階精講1
總結:Java 集合進階精講2-ArrayList
Java集合框架圖

我們經常使用的Arrayist、LinkedList繼承的關系挺復雜的,但繼承的都是接口或抽象類。而Collection和List是接口,Collection接口定義了集合的通用方法,和List接口是在Collection基礎上補充了專屬於List的通用方法。我們什么時候使用抽象類?很多情況是為子類提供共同的方法實現或屬性時會使用抽象類。所以就不難理解AbstractColection和AbstractList的作用了,當然,你也可以繼承於它們實現自己的List
整理后的圖

List子類
- ArrayList
- Vector和Stack
- LinkedList
- SynchronizedList
ArrayLIst
1:ArrayList基於數組實現,訪問元素效率快,插入刪除元素效率慢
ArrayList是基於數組實現的,ArrayList內部維護一個數組elementData,用於保存列表元素,基於數組的數組這數據結構,我們知道,其索引元素是非常快的:
public E get(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); return (E) elementData[index]; // 索引無需遍歷,效率非常高! }
public E set(int index, E element) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); E oldValue = (E) elementData[index]; elementData[index] = element; // 索引無需遍歷,效率非常高! return oldValue; }
get、
set直接根據索引獲取了目標元素,中間不用做任何的遍歷操作,效率是非常快的。
public void add(int index, E element) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); ensureCapacityInternal(size + 1); // 先判斷是否需要擴容 System.arraycopy(elementData, index, elementData, index + 1, // 把index后面的元素都向后偏移一位 size - index); elementData[index] = element; size++; }
從插入操作的源碼可以看到,插入前,要先判斷是否需要擴容(擴容后面會講,這里先跳過),然后把Index后面的元素都偏移一位,這里的偏移是需要把元素復制后,再賦值當前元素的后一索引的位置。顯然,這樣一來,插入一個元素,牽連到多個元素,效率自然就低了。再來看看刪除操作:
public E remove(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); modCount++; E oldValue = (E) elementData[index]; int numMoved = size - index - 1; if (numMoved > 0) { // 把index后面的元素向前偏移一位,填補刪除的元素 System.arraycopy(elementData, index + 1, elementData, index, numMoved); } elementData[--size] = null; // clear to let GC do its work return oldValue; }
同樣,刪除一個元素,需要把index后面的元素向前偏移一位,填補刪除的元素,也是牽連了多個元素。所以在使用時要謹慎了!
2:ArrayList支持快速隨機訪問
什么是隨機訪問?我們不防先來看看ArrayList的類定義:
看到
RandomAccess了嗎,這個就是支持快速隨機訪問的標記,我們再點進去看看其源碼:
/** * ... * <p>It is recognized that the distinction between random and sequential * access is often fuzzy. For example, some <tt>List</tt> implementations * provide asymptotically linear access times if they get huge, but constant * access times in practice. Such a <tt>List</tt> implementation * should generally implement this interface. As a rule of thumb, a * <tt>List</tt> implementation should implement this interface if, * for typical instances of the class, this loop: * <pre> * for (int i=0, n=list.size(); i < n; i++) * list.get(i); * </pre> * runs faster than this loop: * <pre> * for (Iterator i=list.iterator(); i.hasNext(); ) * i.next(); * </pre> * ... */ public interface RandomAccess { }
額,是一個接口,沒有任何的屬性或方法定義。其實它只是一個標記,繼承於它就相當於告訴別人,我支持快速隨機訪問,上面代碼我特意留下部分的注釋說明,其中關鍵的部分在說,通常情況下,使用索引訪問的效率比使用迭代器訪問的效率快!
我們把目光暫時轉移到Collections類下,其中有很多基於是否有繼承於RandomAccess的List做不同的算法選擇判斷,我們來看其中的二分查找算法:
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) { if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) // 當List實現了RandomAccess或小於一定閥值時,使用索引二分查找算法 return Collections.indexedBinarySearch(list, key); else return Collections.iteratorBinarySearch(list, key); }
所以快速隨機訪問是針對於Collections中的方法而言的(其他類是否也有?歡迎大神們補充),支持快速隨機訪問時,就選擇索引訪問,效率會很快。
另外,從上面的二分查找算法我們又能得到一個提高效率的小細節:我們知道List是提供了IndexOf和lastIndexOf方法來檢索元素的,它們分別是從頭和尾開始,一個一個比較的,那么顯然,使用Collections#binarySearch在大多數情況效率會比
IndexOf和lastIndexOf更快~
3:大多數情況下,我們都應該指定ArrayList的初始容量
如果說上面所介紹的細節大部分童鞋都知道,那這個細節相信很多人都不知道,包括在看源碼之前的我。在講為什么之前,我們需要先來了解ArrayList的擴容機制。
ArrayList每次擴容至少為原來容量大小的1.5倍,其默認容量是10,當你不為其指定初始容量時,它就會創建默認容量大小為10的數組:
// 默認最小容量 private static final int DEFAULT_CAPACITY = 10; // 空數組 private static final Object[] EMPTY_ELEMENTDATA = {}; // 默認容量空數組,可以理解為一個標記 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 指定最小容量創建列表 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } // 創建默認空列表 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 默認容量空數組 }
ArrayList的默認構造函數來創建實例,等等,不是說不指定初始容量會創建默認容量大小為10的數組嗎?但這里只賦值了空數組。是的,還記得我們上面分析的
add源碼有個擴容操作嗎?
如果使用默認構造函數來創建實例,在第一次添加元素時,就會進行擴容,擴容到默認容量10的數組:
// 每次添加元素都會調用 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 如果為默認容量空數組的話,添加元素時,至少擴容到默認最小容量 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) // 大於當前容量就擴容 grow(minCapacity); } // 擴容 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍原來大小 // 先嘗試擴容到1.5倍原來容量的大小,如果比用戶指定的大,那么就擴容1.5倍 // 否則擴容用戶指定的 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
所謂“擴容”就是創建一個長度更大的數組,再把舊數組的元素全部賦值到新數組。顯然,這個操作效率也是不理想的。雖然使用默認構造函數創建的實例,在第一次添加元素的擴容並沒有元素復制,但還是要另外創建一個數組,並且是大小為10的數組,可能你並不需要這么大的數組,可能是3,可能是5,那么我們為何不一開始就指定其容量呢?
指定初始容量的方法也很簡單,我們使用帶int參數的構造函數就可以了:
// 指定最小容量創建列表 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
或者有童鞋會說,使用ensureCapacity指定容量也行,其實不然,為何ensureCapacity對容量大小有限制:
// 指定最小容量 public void ensureCapacity(int minCapacity) { int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; // 指定最小容量成功的情況 // 1.使用 new ArrayList() 創建實例並添加元素前,指定容量大小不能小於默認容量10 // 2.列表已存在元素,指定容量大小不能小於當前容量大小 if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } }
所以講到這,相信大家有答案了,為什么創建ArrayList要指定其初始容量?顯然我們是不希望它進行耗時的擴容操作,並且能在我們預知的情況下盡量使用大小剛剛好的列表,而不浪費任何資源。那么我們可以得到以下經驗:
- 都不應該使用默認構造函數創建實例,以免自動擴容到默認最小容量(10)
- 當列表容量確定,應該指定容量的方式創建實例
- 當列表容量不確定時,可以預估我們將有會多少元素,指定稍大於預估值的容量
Vector和Stack
Vector和Stack我們幾乎是不使用的了,所以並不打算用大篇幅來介紹,我們大概了解下就可以了。但我們可以探索下他們為何不受待見,從而引以為戒。
1:Vector也是基於數組實現,同樣支持快速訪問,並且線程安全
因為跟ArrayList一樣,都是基於數組實現,所以ArrayList具有的優勢和劣勢Vector同樣也有,只是Vector在每個方法都加了同步鎖,所以它是線程安全的。但我們知道,同步會大大影響效率的,所以在不需要同步的情況下,Vector的效率就不如ArrayList了。所以我們在不需要同步的情況下,優先選擇ArrayList;而在需要同步的情況下,也不是使用Vector,而是使用SynchronizedList(后面講到)。你看,Vector處於一個很尷尬的地步。但我個人覺得,Vector被遺棄的最大原因不在於它線程同步影響效率——因為這畢竟能在多線程環境下使用——而在於它的擴容機制上。
2:Vector的擴容機制不完善
Vector默認容量也是10,跟ArrayList不同的是,Vector每次擴容的大小是可以指定的,如果不指定,每次擴容原來容量大小的2倍:
protected Object[] elementData; // 元素數組 protected int elementCount; // 元素數量 protected int capacityIncrement; // 擴容大小 public Vector(int initialCapacity, int capacityIncrement) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; this.capacityIncrement = capacityIncrement; } public Vector(int initialCapacity) { this(initialCapacity, 0); // 默認擴容大小為0,那么擴容時會增大兩倍 } public Vector() { this(10); // 默認容量為10 } public synchronized void ensureCapacity(int minCapacity) { if (minCapacity > 0) { modCount++; ensureCapacityHelper(minCapacity); } } private void ensureCapacityHelper(int minCapacity) { // overflow-conscious code if (minCapacity - elementData.length > 0) // 大於當前容量就擴容 grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); // 默認擴容兩倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
另外需要提醒注意的是,不像ArrayList,如果是用Vector的默認構造函數創建實例,那么第一次添加元素就需要擴容,但不會擴容到默認容量10,只會根據用戶指定或兩倍的大小擴容。所以使用Vector時指不指定擴容大小都很尷尬:
- 如果容量大小和擴容大小都不指定,開始可能會頻繁地進行擴容
- 如果指定了容量大小不指定擴容大小,以2倍的大小擴容會浪費很多資源
- 如果指定了擴容大小,擴容大小就固定了,不管數組多大,都按這大小來擴容,那么這個擴容大小的取值總有不理想的時候
從Vector我們也可以反觀ArrayList設計巧妙的地方,這也許是Vector存在的唯一價值了哈哈。
3:Stack繼承於Vector,在其基礎上擴展了棧的方法
Stack我們也不使用了,它只是添加多幾個棧常用的方法(這個LinkedList也有,后面討論),簡單來看下它們的實現吧:
// 進棧 public E push(E item) { addElement(item); return item; } // 出棧 public synchronized E pop() { E obj; int len = size(); obj = peek(); removeElementAt(len - 1); return obj; } public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); return elementAt(len - 1); }
LinkedList
再來看看我們熟悉的LinkedList~
1:LinkedList基於鏈表實現,插入刪除元素效率快,訪問元素效率慢
LinkedList內部維護一個雙端鏈表,可以從頭開始檢索,也可以從尾開始檢索。同樣的,得益於鏈表這一數據結構,LinkedList在插入和刪除元素效率非常快。
插入元素只需新建一個node,再把前后指針指向對應的前后元素即可:
// 鏈尾追加 void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } // 指定節點前插入 void linkBefore(E e, Node<E> succ) { // assert succ != null; // 插入節點,succ為Index的節點,可以看到,是插入到index節點的前一個節點 final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; if (pred == null) first = newNode; else pred.next = newNode; size++; modCount++; } public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); }
同樣,刪除元素只要把刪除節點的鏈剪掉,再把前后節點連起來就搞定了:
E unlink(Node<E> x) { // assert x != null; final E element = x.item; final Node<E> next = x.next; final Node<E> prev = x.prev; if (prev == null) { // 鏈頭 first = next; } else { prev.next = next; x.prev = null; } if (next == null) { // 鏈尾 last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; } public E remove(int index) { checkElementIndex(index); return unlink(node(index)); }
Node<E> node(int index) { // 使用了二分法 if (index < (size >> 1)) { // 如果索引小於二分之一,從first開始遍歷 Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { // 如果索引大於二分之一,從last開始遍歷 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } } public E get(int index) { checkElementIndex(index); return node(index).item; }
所以,LinkedList和ArrayList剛好是互補的,所以具體場景,應考慮哪種操作最頻繁,從而選擇不同的List來使用。
2:LinkedList可以當作隊列和棧來使用
不知大家有沒注意到在圖2.2中,LinkedList非常“特立獨行地”繼承了Deque接口,而Deque又繼承於Queue接口,這隊列和棧的方法定義就是在這些接口中定義的,而LinkedList實現其方法,使自身具備了隊列的棧的功能。
當作隊列(先進先出)使用:
// 進隊 public boolean offerFirst(E e) { addFirst(e); return true; } // 出隊 public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l); }
// 進棧 public void push(E e) { addFirst(e); } // 出棧,如果為空列表,會拋出異常 public E pop() { return removeFirst(); }
SynchronizedList
在Collections類中提供了很多線程線程的集合類,其實他們實現很簡單,只是在集合操作前,加一個鎖而已。
1:SynchronizedList繼承於SynchronizedCollection,使用裝飾者模式,為原來的List加上鎖,從而使List同步安全
先來看下SynchronizedCollection的定義:
static class SynchronizedCollection<E> implements Collection<E>, Serializable { private static final long serialVersionUID = 3053995032091335093L; final Collection<E> c; // 裝飾的集合 final Object mutex; // 鎖 SynchronizedCollection(Collection<E> c) { this.c = Objects.requireNonNull(c); mutex = this; } SynchronizedCollection(Collection<E> c, Object mutex) { this.c = Objects.requireNonNull(c); this.mutex = Objects.requireNonNull(mutex); } }
可以看到,可以指定一個對象作為鎖,如果不指定,默認就鎖了集合了。
再來看下我們關注的SynchronizedList:
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { final List<E> list; SynchronizedList(List<E> list) { super(list); this.list = list; } SynchronizedList(List<E> list, Object mutex) { super(list, mutex); this.list = list; } ... public E get(int index) { synchronized (mutex) {return list.get(index);} } public E set(int index, E element) { synchronized (mutex) {return list.set(index, element);} } public void add(int index, E element) { synchronized (mutex) {list.add(index, element);} } public E remove(int index) { synchronized (mutex) {return list.remove(index);} } ... }
想不到SynchronizedList的實現是如此簡單,上面的源碼想必不用我多說了。
小結:
ArrayList和LinkedList 各有優勢,應根據具體場景從優選擇- 根據
ArrayList的擴容機制,開始就指定其初始容量,避免資源浪費 LinkedList可以當作隊列和棧使用,也可以進一步封裝- 不推薦使用
Vector和Stack,同步場景下,使用SynchronizedList替代
