java核心技術點之集合框架

概述

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):


轉載於:http://blog.csdn.net/qq_35101189/article/details/54892054
發佈了111 篇原創文章 · 獲贊 68 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章