Java核心技術點之集合框架

absfree發表於2016-09-05

概述

Java集合框架由Java類庫的一系列介面、抽象類以及具體實現類組成。我們這裡所說的集合就是把一組物件組織到一起,然後再根據不同的需求操縱這些資料。集合型別就是容納這些物件的一個容器。也就是說,最基本的集合特性就是把一組物件放一起集中管理。根據集合中是否允許有重複的物件、物件組織在一起是否按某種順序等標準來劃分的話,集合型別又可以細分為許多種不同的子型別。

Java集合框架為我們提供了一組基本機制以及這些機制的參考實現,其中基本的集合介面是Collection介面,其他相關的介面還有Iterator介面、RandomAccess介面等。這些集合框架中的介面定義了一個集合型別應該實現的基本機制,Java類庫為我們提供了一些具體集合型別的參考實現,根據對資料組織及使用的不同需求,只需要實現不同的介面即可。Java類庫還為我們提供了一些抽象類,提供了集合型別功能的部分實現,我們也可以在這個基礎上去進一步實現自己的集合型別。

Collection介面

迭代器

我們先來看下這個介面的定義:

public interface Collection<E> extends Iterable<E>

首先,它使用了一個型別引數;其次,它實現了Iterable<E>介面,我們再來看下Iterable<E>介面的定義:

public interface Iterable<T> {
  Iterator<T> iterator();
}

我們可以看到這個介面只定義了一個方法,這個方法要求我們返回一個實現了Iterator<T>型別的物件,所以我們看下Iterator<T>的定義:

public interface Iterator<E> { 
  boolean hasNext(); 
  E next(); 
  void remove();
}

說到這裡,我們簡單地說一下迭代器(Iterator)這個東西。上面我們一共提到了兩個和迭代器相關的介面:Iterable<E>介面和Iterator<E>介面,從字面意義上來看,前者的意思是“可迭代的”,後者的意思是“迭代器。所以我們可以這麼理解這兩個介面:實現了Iterable<E>介面的類是可迭代的;實現了Iterator<E>介面的類是一個迭代器

迭代器就是一個我們用來遍歷集合中的物件的東西。也就是說,對於集合,我們不是像對原始型別陣列那樣通過陣列索引來直接訪問相應位置的元素,而是通過迭代器來遍歷。這麼做的好處是將對於集合型別的遍歷行為與被遍歷的集合物件分離,這樣一來我們無需關心該集合型別的具體實現是怎樣的。只要獲取這個集合物件的迭代器, 便可以遍歷這個集合中的物件了。而像遍歷物件的順序這些細節,全部由它的迭代器來處理。現在我們來梳理一下前面提到的這些東西:首先,Collection介面實現了Iterable<E>介面,這意味著所有實現了Collection介面的具體集合類都是可迭代的。那麼既然要迭代,我們就需要一個迭代器來遍歷相應集合中的物件,所以Iterable<E>介面要求我們實現iterator方法,這個方法要返回一個迭代器物件。一個迭代器物件也就是實現了Iterator<E>介面的物件,這個介面要求我們實現hasNext()、next()、remove()這三個方法。其中hasNext方法判斷是否還有下一個元素(即是否遍歷完物件了),next方法會返回下一個元素(若沒有下一個元素了呼叫它會引起丟擲一個NoSuchElementException異常),remove方法用於移除最近一次呼叫next方法返回的元素(若沒有呼叫next方法而直接呼叫remove方法會報錯)。我們可以想象在開始對集合進行迭代前,有個指標指向集合第一個元素的前面,第一次呼叫next方法後,這個指標會”掃過”第一個元素並返回它,呼叫hasNext方法就是看這個指標後面還有沒有元素了。也就是說這個指標始終指向剛遍歷過的元素和下一個待遍歷的元素之間。通常,迭代一個集合物件的程式碼是這個樣子的:

Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while (iter.hasNext()) {
  String element = iter.next();
  //do something with element
}

從Java SE 5.0開始,我們可以使用與以上程式碼段等價但是更加簡潔的版本:

for (String element : c) {
  //do something with element
}

上面我們提到過Iterator介面的remove方法必須在next方法返回一個元素後才能呼叫,這對Java類庫中為我們提供的實現了Collection介面的類來說是這樣的。當然我們可以通過自己定義一個實現Collection介面的集合類來改變這一預設行為(除非有充足的理由,否則最好不要這樣做)。

Collection介面

我們先來看一下它的官方定義:

The root interface in the collection hierarchy. A collection represents a group of objects, known as its elements. Some collections allow duplicate elements and others do not. Some are ordered and others unordered. The JDK does not provide any direct implementations of this interface: it provides implementations of more specific subinterfaces like Set
and List.

大概的意思就是:Collection介面是集合層級結構的根介面。一個集合代表了一組物件,這組物件被稱為集合的元素。一些集合允許重複的元素而其他不允許;一些是有序的而一些是無序的。Java類庫中並未提供任何對這個介面的直接實現,而是提供了對於它的更具體的子介面的實現(比如Set介面和List介面)。

我們知道,介面是一組對需求的描述,那麼讓我們看看Collection介面提出了哪些需求。Collection介面中定義了以下方法:

boolean add(E e) //向集合中新增一個元素,若新增元素後集合發生了變化就返回true,若沒有發生變化,就返回false。(optional operation).
boolean addAll(Collection<? extends E> c) //新增給定集合c中的所有元素到該集合中(optional operation).
void clear() //(optional operation).
boolean contains(Object o) //判斷該集合中是否包含指定物件
boolean containsAll(Collection<?> c)
boolean equals(Object o)
int hashCode()
boolean isEmpty()
Iterator<E> iterator()
boolean remove(Object o) //移除給定物件的一個例項(有的具體集合型別允許重複元素) (optional operation).
boolean removeAll(Collection<?> c) //(optional operation).
boolean retainAll(Collection<?> c) //僅保留給定集合c中的元素(optional operation).
int size()
Object[] toArray()
<T> T[] toArray(T[] a)

我們注意到有些方法後面註釋中標註了“optional operation”,意思是Collection介面的實現類究竟需不需要實現這個方法視具體情況而定。比如有些具體的集合型別不允許向其中新增物件,那麼它就無需實現add方法。我們可以看到,Collection物件必須實現的方法有:contains方法、containsAll方法、isEmpty方法、iterator方法、size方法、兩個toArray方法以及equals方法、hashCode方法,其中最後兩個方法繼承自Object類。

我們來說一下兩個toArray方法,它們的功能都是都是返回這個集合的物件陣列。第二個方法接收一個arrayToFill引數,當這個引數陣列足夠大時,就把集合中的元素都填入這個陣列(多餘空間填null);當arrayToFill不夠大時,就會建立一個大小與集合相同,型別與arrayToFill相同的陣列,並填入集合元素。

Collection介面的直接子介面主要有三個:List介面、Set介面和Queue介面。下面我們對它們進行逐一介紹。

List介面

我們同樣先看下它的官方定義:

An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.Unlike sets, lists typically allow duplicate elements. More formally, lists typically allow pairs of elements e1 and e2 such that e1.equals(e2), and they typically allow multiple null elements if they allow null elements at all.

大概意思是:List是一個有序的集合型別(也被稱作序列)。使用List介面可以精確控制每個元素被插入的位置,並且可以通過元素在列表中的索引來訪問它。列表允許重複的元素,並且在允許null元素的情況下也允許多個null元素。

我們再來看下它定義了哪些方法:

ListIterator<E> listIterator();
void add(int i, E element);
E remove(int i);
E get(int i);
E set(int i, E element);
int indexOf(Object element);

我們可以看到,列表支援對指定位置元素的讀寫與移除。我們注意到,上面有一個listIterator方法,它返回一個列表迭代器。我們來看一看ListIterator<E>介面都定義了哪些方法:

void add(E e) //在當前位置新增一個元素
boolean hasNext() //返回ture如果還有下個元素(在正向遍歷列表時使用)
boolean hasPrevious() //反向遍歷列表時使用
E next() //返回下一個元素並將cursor(也就是我們上文提到的”指標“)前移一個位置
int nextIndex() //返回下一次呼叫next方法將返回的元素的索引
E previous() //返回前一個元素並將cursor向前移動一個位置
int previousIndex() //返回下一次呼叫previous方法將返回的元素的索引void remove() //從列表中移除最近一次呼叫next方法或previous方法返回的元素
void set(E e) //用e替換最近依次呼叫next或previous方法返回的元素

ListIterator<E>是Iterator<E>的子介面,它支援像雙向迭代這樣更加特殊化的操作。綜合以上,我們可以看到,List介面支援兩種訪問元素的方式:使用列表迭代器順序訪問或者使用get/set方法隨機訪問。

Java類庫中常見的實現了List<E>介面的類有:ArrayList, LinkedList,Stack,Vector,AbstractList,AbstractSequentialList等等。

ArrayList

ArrayList是一個可動態調整大小的陣列,允許null型別的元素。我們知道,Java中的陣列大小在初始化時就必須確定下來,而且一旦確定就不能改變,這會使得在很多場景下不夠靈活。ArrayList很好地幫我們解決了這個問題,當我們需要一個能根據包含元素的多少來動態調整大小的陣列時,那麼ArrayList正是我們所需要的。

我們先來看看這個類的常用方法:

boolean add(E e) //新增一個元素到陣列末尾
void add(int index, E element) //新增一個元素到指定位置
void clear()
boolean contains(Object o)
void ensureCapacity(int minCapacity) //確保ArrayList至少能容納引數指定數目的物件,若有需要會增加ArrayList例項的容量。
E get(int index) //返回指定位置的元素
int indexOf(Object o)
boolean isEmpty()
Iterator<E> iterator()
ListIterator<E> listIterator()
E remove(int index)
boolean remove(Object o)
E set(int index, E element)
int size()

當我們插入了比較多的元素,導致ArrayList快要裝滿時,它會自動增長容量。ArrayList內部使用一個Object陣列來儲存元素,自動增長容量是通過建立一個新的容量更大的Object陣列,並將元素從原Object陣列複製到新Object陣列來實現的。若要想避免這種開銷,在知道大概會容納多少資料時,我們可以在構造時指定好它的大小以儘量避免它自動增長的發生;我們也可以呼叫ensureCapacity方法來增加ArrayList物件的容量到我們指定的大小。ArrayList有以下三個構造器:

ArrayList()
ArrayList(Collection<? extends E> c)
ArrayList(int initialCapacity) //指定初始capacity,即內部Object陣列的初始大小
LinkedList類

LinkedList類代表了一個雙向連結串列,允許null元素。這個類同ArrayList一樣,不是執行緒安全的。
這個類中主要有以下的方法:

void addFirst(E element);
void addLast(E element);
E getFirst();
E getLast();
E removeFirst();
E removeLast();

這些方法的含義正如它們的名字所示。LinkedList作為List介面的實現類,自然包含了List介面中定義的add等方法。LinkedList的add方法實現有以下兩種:

boolean add(E e) //把元素e新增到連結串列末尾
void add(int index, E element) //在指定索引處新增元素

LinkedList的一個缺陷在於它不支援對元素的高效隨機訪問,要想隨機訪問其中的元素,需要逐個掃描直到遇到符合條件的元素。只有當我們需要減少在列表中間新增或刪除元素操作的代價時,可以考慮使用LinkedList。

Set介面

Set介面與List介面的重要區別就是它不支援重複的元素,至多可以包含一個null型別元素。Set介面定義的是數學意義上的“集合”概念。
Set介面主要定義了以下方法:

boolean add(E e)
void clear()
boolean contains(Object o)
boolean isEmpty()
boolean equals(Object obj)
Iterator<E> iterator()
boolean remove(Object o)
boolean removeAll(Collection<?> c)
int size()
Object[] toArray()
<T> T[] toArray(T[] a)

Set介面並沒有顯式要求其中的元素是有序或是無序的,它有一個叫做SortedSet的子介面,這個介面可以用來實現對Set元素的排序,SortedSet還有叫做NavigableSet的子介面,這個介面定義的方法可以在有序Set中進行查詢和遍歷。Java類庫中實現了Set介面的類主要有:AbstractSet,HashSet,TreeSet,EnumSet,LinkedHashSet等等。其中,HashSet與TreeSet都是AbstractSet的子類。那麼,為什麼Java類庫要提供AbstractSet這個抽象類呢?答案是為了讓我們在自定義實現Set介面的類時不必“從零開始”,AbstractSet這個抽象類已經為我們實現了Set介面中的一些常規方法,而一些靈活性比較強的方法可以由我們自己來定義,我們只需要繼承AbstractSet這個抽象類即可。類似的抽象類還有很多,比如我們上面提到的實現了List介面的AbstractList抽象類就是LinkedList和ArrayList的父類。Java官方文件中提到,HashSet和TreeSet分別基於HashMap和TreeMap實現(我們在後面會簡單介紹HashMap和TreeMap),他們的區別在於Set<E>介面是一個物件的集(數學意義上的”集合“),Map<K, V>是一個鍵值對的集合。而且由於它們分別是對Set<E>和Map<K, V>介面的實現,相應新增與刪除元素的方法也取決於具體介面的定義。

Queue介面

Queue介面是對佇列這種資料結構的抽象。一般的佇列實現允許我們高效的在隊尾新增元素,在佇列頭部刪除元素(First in, First out)。Queue<E>介面還有一個名為Deque的子介面,它允許我們高效的在隊頭或隊尾新增/刪除元素,實現了Deque<E>的介面的集合類即為雙端佇列的一種實現(比如LinkedList就實現了Deque介面)。Queue介面定義了以下方法:

boolean add(E e) //新增一個元素到佇列中,若佇列已滿會丟擲一個IllegalStateException異常
E element() //獲取隊頭元素
boolean offer(E e) //新增一個元素到佇列中,若佇列已滿返回false
E peek() //獲取隊頭元素,若佇列為空返回null
E poll() //返回並移除隊頭元素,若佇列為空返回null
E remove() //返回並移除隊頭元素

我們注意觀察下上面的方法:add與offer,element與peek,remove與poll看似是三對兒功能相同的方法。它們之間的重要區別在於前者若操作失敗會丟擲一個異常,後者若操作失敗會從返回值體現出來(比如返回false或null),我們可以根據具體需求呼叫它們中的前者或後者。

實現Queue介面的類主要有:AbstractQueue, ArrayDeque, LinkedList,PriorityQueue,DelayQueue等等。關於它們具體的介紹可參考官方文件或相關的文章。

Map介面

我們先來看下它的定義:

An object that maps keys to values. A map cannot contain duplicate keys; each key can map to at most one value.The Map
interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings. The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. Some map implementations, like the TreeMap
class, make specific guarantees as to their order; others, like the HashMap
class, do not.

大概意思是這樣的:一個把鍵對映到值的物件被稱作一個Map物件。對映表不能包含重複的鍵,每個鍵至多可以與一個值關聯。Map介面提供了三個集合檢視(關於集合檢視的概念我們下面會提到):鍵的集合檢視、值的集合檢視以及鍵值對的集合檢視。一個對映表的順序取決於它的集合檢視的迭代器返回元素的順序。一些Map介面的具體實現(比如TreeMap)保證元素有一定的順序,其它一些實現(比如HashMap)則不保證元素在其內部有序。

也就是說,Map介面定義了一個類似於“字典”的規範,讓我們能夠根據鍵快速檢索到它所關聯的值。我們先來看看Map介面定義了哪些方法:

void clear()
boolean containsKey(Object key) //判斷是否包含指定鍵
boolean containsValue(Object value) //判斷是否包含指定值
boolean isEmpty()
V get(Object key) //返回指定鍵對映的值
V put(K key, V value) //放入指定的鍵值對
V remove(Object key)
int size()
Set<Map.Entry<K,V>> entrySet() 
Set<K> keySet()
Collection<V> values()

後三個方法在我們下面介紹集合檢視時會具體講解。

Map介面的具體實現類主要有:AbstractMap,EnumMap,HashMap,LinkedHashMap,TreeMap。HashTable。

HashMap

我們看一下HashMap的官方定義:

HashMap<K, V>是基於雜湊表這個資料結構的Map介面具體實現,允許null鍵和null值。這個類與HashTable近似等價,區別在於HashMap不是執行緒安全的並且允許null鍵和null值。由於基於雜湊表實現,所以HashMap內部的元素是無序的。HashMap對與get與put操作的時間複雜度是常數級別的(在雜湊均勻的前提下)。對HashMap的集合檢視進行迭代所需時間與HashMap的capacity(bucket的數量)加上HashMap的尺寸(鍵值對的數量)成正比。因此,若迭代操作的效能很重要,不要把初始capacity設的過高(不要把load factor設的過低)。

有兩個因素會影響一個HashMap物件的效能:intial capacity(初始容量)和load factor(負載因子)。intial capacity就是HashMap物件剛建立時其內部的雜湊表的“桶”的數量(請參考雜湊表的定義)。load factor等於maxSize / capacity,也就是HashMap所允許的最大鍵值對數與桶數的比值。增大load factor可以節省空間但查詢一個元素的時間會增加,減小load factor會佔用更多的儲存空間,但是get與put的操作會更快。當HashMap中的鍵值對數量超過了maxSize(即load factor與capacity的乘積),它會再雜湊,再雜湊會重建內部資料結構,桶數(capacity)大約會增加到原來的兩倍。

HashMap預設的load factor大小為0.75,這個數值在時間與空間上做了很好的權衡。當我們清楚自己將要大概存放多少資料時,也可以自定義load factor的大小。

HashMap的構造器如下:

HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
HashMap(Map<? extends K,? extends V> m) //建立一個新的HashMap,用m的資料填充

常用方法如下:

void clear()
boolean containsKey(Object key)
boolean containsValue(Object value)
V get(Object key)
V put(K key, V value)
boolean isEmpty()
V remove(Object key)
int size()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()
Set<K> keySet()

它們的功能都很直觀,更多的使用細節可以參考Java官方文件,這裡就不貼上來了。這裡簡單地提一下WeakHashMap,它與HashMap的區別在於,儲存在其中的key是“弱引用”的,也就是說,當不再存在對WeakHashMap中的鍵的外部引用時,相應的鍵值對就會被回收。關於WeakHashMap和其他類的具體使用方法及注意事項,大家可以參考官方文件。下面我們來簡單地介紹下另一個Map介面的具體實現——TreeMap。

TreeMap

它的官方定義是這樣的:

TreeMap<K, V>一個基於紅黑樹的Map介面實現。TreeMap中的元素的有序的,排序的依據是儲存在其中的鍵的natural ordering(自然序,也就是數字從小到大,字母的話按照字典序)或者根據在建立TreeMap時提供的Comparator物件,這取決於使用了哪個構造器。TreeMap的containsKey, get, put和remove操作的時間複雜度均為log(n)。

TreeMap有以下構造器:

TreeMap() //使用自然序對其元素進行排序
TreeMap(Comparator<? super K> comparator) //使用一個比較器對其元素進行排序
TreeMap(Map<? extends K,? extends V> m) //構造一個與對映表m含有相同元素的TreeMap,用自然序進行排列
TreeMap(SortedMap<K,? extends V> m) //構造一個與有序對映表m含有相同元素及元素順序的TreeMap

它的常見方法如下:

Map.Entry<K,V> ceilingEntry(K key) //返回一個最接近且大於等於指定key的鍵值對。
K ceilingKey(K key)
void clear()
Comparator<? super K> comparator() //返回使用的比較器,若按自然序則返回null
boolean containsKey(Object key)
boolean containsValue(Object value)
NavigableSet<K> descendingKeySet() //返回一個包含在TreeMap中的鍵的逆序的NavigableSet檢視
NavigableMap<K,V> descendingMap()
Set<Map.Entry<K,V>> entrySet()
Map.Entry<K,V> firstEntry() //返回鍵最小的鍵值對
Map.Entry<K,V> floorEntry(K key) //返回一個最接近指定key且小於等於它的鍵對應的鍵值對
K floorKey(K key)
V get(Object key)
Set<K> keySet()
Map.Entry<K,V> lastEntry() //返回與最大的鍵相關聯的鍵值對
K lastKey()

建議大家先了解下紅黑樹這個資料結構的原理及實現(可參考演算法(第4版) (豆瓣)),然後再去看官方文件中關於這個類的介紹,這樣學起來會事半功倍。

最後再簡單地介紹下NavigableMap<K, V>這個介面:

實現了這個介面的類支援一些navigation methods,比如lowerEntry(返回小於指定鍵的最大鍵所關聯的鍵值對),floorEntry(返回小於等於指定鍵的最大鍵所關聯的鍵值對),ceilingEntry(返回大於等於指定鍵的最小鍵所關聯的鍵值對)和higerEntry(返回大於指定鍵的最小鍵所關聯的鍵值對)。一個NavigableMap支援對其中儲存的鍵按鍵的遞增順序或遞減順序的遍歷或訪問。NavigableMap<K, V>介面還定義了firstEntry、pollFirstEntry、lastEntry和pollLastEntry等方法,以準確獲取指定位置的鍵值對。

總的來說,NavigableMap<K, V>介面正如它的名字所示,支援我們在對映表中”自由的航行“,正向或者反向迭代其中的元素並獲取我們需要的指定位置的元素。TreeMap實現了這個介面。

檢視(View)與包裝器

下面我們來解決一個上面遺留的問題,也就是介紹一下集合檢視的概念。Java中的集合檢視是用來檢視集合中全部或部分資料的一個”視窗“,只不過通過檢視我們不僅能檢視相應集合中的元素,對檢視的操作還可能會影響到相應的集合。通過使用檢視可以獲得其他的實現了Map介面或Collection介面的物件。比如我們上面提到的TreeMap和HashMap的keySet()方法就會返回一個相應對映表物件的檢視。也就是說,keySet方法返回的檢視是一個實現了Set介面的物件,這個物件中又包含了一系列鍵物件。

輕量級包裝器

Arrays.asList會發揮一個包裝了Java陣列的集合檢視(實現了List介面)。請看以下程式碼:

public static void main(String[] args) {
  String[] strings = {"first", "second", "third"};
  List<String> stringList = Arrays.asList(strings);
  String s1 = stringList.get(0);
  System.out.println(s1);
  stringList.add(0, "new first");
}

以上程式碼會編譯成功,但是在執行時會丟擲一個UnsupportedOperationException異常,原因是呼叫了改變列表大小的add方法。Arrays.asList方法返回的封裝了底層陣列的集合檢視不支援對改變陣列大小的方法(如add方法和remove方法)的呼叫(但是可以改變陣列中的元素)。實際上,這個方法呼叫了以下方法:

Collections.nCopies(n, anObject);

這個方法會返回一個實現了List介面的不可修改的物件。這個物件包含了n個元素(anObject)。

子範圍

我們可以為很多集合型別建立一個稱為子範圍(subrange)的集合檢視。例如以下程式碼抽出group中的第10到19個元素(從0開始計數)組成一個子範圍:

List subgroup = group.subList(10, 20); //group為一個實現了List介面的列表型別

List介面所定義的操作都可以應用於子範圍,包括那些會改變列表大小的方法,比如以下方法會把subgroup列表清空,同時group中相應的元素也會從列表中移除:

subgroup.clear();

對於實現了SortedSet<E>介面的有序集或是實現了SortedMap<K, V>介面的有序對映表,我們也可以為他們建立子範圍。SortedSet介面定義了以下三個方法:

SortedSet<E> subSet(E from, E to); 
SortedSet<E> headSet(E to);
SortedSet<E> tailSet(E from);

SortedMap也定義了類似的方法:

SortedMap<K, V> subMap(K from, K to);
SortedMap<K, V> headMap(K to);
SortedMap<K, V> tailMap(K from);

不可修改的檢視

Collections類中的一些方法可以返回不可修改檢視(unmodifiable views):

Collections.unmodifiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap

同步檢視

若集合可能被多個執行緒併發訪問,那麼我們就需要確保集合中的資料不會被破壞。Java類庫的設計者使用檢視機制來確保常規集合的執行緒安全。比如,我們可以呼叫以下方法將任意一個實現了Map介面的集合變為執行緒安全的:

Map<String, Integer> map = Collections.synchronizedMap(new HashMap<String, Integer>());

被檢驗檢視

我們先看一下這段程式碼:

ArrayList<String> strings = new ArrayList<String>();
ArrayList rawList = strings;
rawList.add(new Date());

在以上程式碼的第二行,我們把泛型陣列賦值給了一個原始型別陣列,這通常只會產生一個警告。而第三行我們往rawList中新增一個Date物件時,並不會產生任何錯誤。因為rawList內部儲存的實際上是Object物件,而任何物件都可以轉換為Object物件。那麼我們怎麼避免這一問題呢,請看以下程式碼:

ArrayList<String> strings = new ArrayList<String>();
List<String> safeStrings = Collections.checkedList(strings, String.class);
ArrayList rawList = safeStrings;
rawList.add(new Date()); //Checked list throws a ClassCastException

在上面,我們通過包裝strings得到一個被檢驗檢視safeStrings。這樣在嘗試新增非String物件時,便會丟擲一個ClassCastException異常。

集合檢視的本質

集合檢視本身不包含任何資料,它只是對相應介面的包裝。集合檢視所支援的所有操作都是通過訪問它所關聯的集合類例項來實現的。我們來看看HashMap的keySet方法的原始碼:

public Set<K> keySet() {
  Set<K> ks;
  return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
} 

final class KeySet extends AbstractSet<K> {
  public final int size() { 
    return size; 
  }
  public final void clear() { 
    HashMap.this.clear(); 
  }
  public final Iterator<K> iterator() { 
    return new KeyIterator(); 
  }
  public final boolean contains(Object o) { 
    return containsKey(o); 
  }
  public final boolean remove(Object key) {
    return removeNode(hash(key), key, null, false, true) != null;
  }
  public final Spliterator<K> spliterator() {
    return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
  }
  public final void forEach(Consumer<? super K> action) {
    Node<K,V>[] tab;
    if (action == null) throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
      int mc = modCount;
      for (int i = 0; i < tab.length; ++i) {
        for (Node<K,V> e = tab[i]; e != null; e = e.next)
          action.accept(e.key);
        }
        if (modCount != mc) throw new ConcurrentModificationException();
      }
  }
}

我們可以看到,實際上keySet()方法返回一個內部final類KeySet的例項。我們可以看到KeySet類本身沒有任何例項變數。我們再看KeySet類定義的size()例項方法,它的實現就是通過直接返回HashMap的例項變數size。還有clear方法,實際上呼叫的就是HashMap物件的clear方法。

keySet方法能夠讓你直接訪問到Map的鍵集,而不需要複製資料或者建立一個新的資料結構,這樣做往往比複製資料到一個新的資料結構更加高效。考慮這樣一個場景:你需要把一個之前建立的陣列傳遞給一個接收List引數的方法,那麼你可以使用Arrays.asList方法返回一個包裝了陣列的檢視(這需要的空間複雜度是常數級別的),而不用建立一個新的ArrayList再把原陣列中的資料複製過去。

Collections類

我們要注意到Collections類與Collection介面的區別:Collection是一個介面,而Collections是一個類(可以看做一個靜態方法庫)。下面我們看一下官方文件對Collections的描述:

Collections類包含了大量用於操作或返回集合的靜態方法。它包含操作集合的多型演算法,還有包裝集合的包裝器方法等等。這個類中的所有方法在集合或類物件為空時均會丟擲一個NullPointerException。

關於Collections類中的常用方法,我們上面已經做了一些介紹,更加詳細的介紹大家可以參考Java官方文件。

總結

關於Java集合框架,我們首先應該把握住幾個核心的介面,請看下圖(下圖中LinkList拼寫有誤,應為LinkedList):

我們還要了解到這些介面描述了一組什麼樣的機制,然後以此作為出發點,去了解具體哪些類實現了哪些機制。像這樣自頂向下的學習,我們很快就能掌握常見集合類的用法。對於一些我們平常經常使用的類,我們還可以閱讀一下它的原始碼,瞭解它的實現細節,這樣我們以後使用起來會更加得心應手。不過閱讀一些集合類(比如TreeMap、HashMap)的原始碼需要我們具備一定的資料結構與演算法的基礎知識,這方面推薦閱讀 演算法(第4版) (豆瓣)

參考資料

  1. 《Java核心技術(卷一)》
  2. What is a view of a collection?
  3. Java SE 7 Docs

相關文章