Java中的容器詳細總結(編輯中)
原文鏈接:http://anxpp.com/index.php/archives/656/
注:本文基於 Jdk1.8 編寫
通常程序總是根據運行時才知道的某些條件去創建新的對象。在此之前,也不知道要創建的對象的數量(甚至是確切的對象類型)。為了解決這個問題,java提供了數組來解決這個問題,但是數組是長度固定的,很多時候簡單的數組是不能滿足我們的需求的,所以java還提供了一套相當完整的容器類來解決這個問題,這也是本文要介紹的。
1、概述
Java容器類類庫的用途是保存對象(引用),主要分兩個概念:
1、Collection:一個獨立元素的序列。List、Set、Queue。
2、Map:一組成對的鍵值對對象。其實ArrayList也可以看做是數字與對象的關聯關系,而如HashMap等,是對象與對象關聯。
List,Set,Queue都是繼承自Collection的,而Collection是繼承Iterable的,這樣的實現可以返回一個迭代器。所以Collection及其子類可以使用迭代器來遍歷數組元素。而對於List,有專門的迭代器,可以用於設置初始迭代位置和雙向遍歷,后面都會慢慢介紹。
Collection的所有操作如下(不含Object類的方法):
檢查Collection中的元素,推薦使用迭代器。
Map並不是繼承自Collection的,后面會單獨介紹。
涉及到hashCode的地方(如HashSet和HashMap)會在1.6中進一步解釋。
涉及到線程同步的相關容器,也會放到后面單獨介紹。
所有常用實現或接口都會給出源碼分析,在文章后面部分會給出源碼分析的索引,讀者可以方便的找到具體實現或接口的源碼。
1.1、List
List可以將元素維護在特定的序列中。List接口在Collection的基礎上添加了大量的方法,使得可以在List的中間插入和移除元素。
有兩種類型的List實現:
- 基本的ArrayList,隨機訪問速度很快,但是在List中間插入和移除元素時較慢
- LinkedList,特性與ArrayList相反,善於順序訪問,在List中間插入刪除代價也較低。但隨機訪問比較慢,特性集比ArrayList更大。
ArrayList
ArrayList是List接口的一種底層采用數組,並可以自動調整容量的實現,因為底層使用了數組,所以隨機訪問元素的時間復雜度是常數級別的,迭代是線性的,但是在其中插入或刪除元素,必須將插入點之后的元素全部前移或者后移,速度就相對比較慢了。
ArrayList默認的初始容量為10,可以使用帶參數的構造函數修改這個默認值,而且數組默認增長的方式是增加原來長度的一半(oldLength>>1),而默認的最大容量為Integer.MAX-8,java的int為4字節的,所以0x7fffffff-8已經是一個相當大的數了。
ArrayList不是線程安全的,多線程同時讀不會出問題,但是如果有一個線程在進行包含寫的操作,就很容易發生錯誤。但是在迭代ArrayList的時候,有快速報錯機制,即在迭代過程中,數組被修改了,將會引發異常,這個異常在next()方法和remove()方法的調用上都可能發生。而這種機制也是有父類提供支持的。
這種機制也不是僅為了多線程設置的,多線程有多線程相關的容器,而且即使用ArrayList,我們也會加上我們自己的同步策略。在單線程中,誰也不能阻止你在遍歷的過程中修改這個表的結構,雖然你並不應該這么做,為了安全,就只能拋個異常了。
ArrayList直接繼承自AbstractList。在AbstractList中有定義這樣一個變量:protected transient int modCount = 0;什么意思呢?其實就是指已從結構上修改此列表的次數。從結構上修改是指更改列表的大小,或者打亂列表。我們知道,這樣的修改很容易導致遍歷發生錯誤的,但是有了這個變量,我們就能提前發現這個隱患,並拋出異常:在調用iterator()返回迭代器時,這個迭代器會記錄此時列表被修改的次數,每次遍歷或刪除元素,都會判斷列表修改次數是不是發生改變,若發生改變就會拋出異常。
關於性能:
- 倘若一開始我們能根據一些條件適當的初始化這個列表默認容量,可能會減少數組自動增長開銷。
- 使用elementData()獲取元素比get()會高效一點,因為不會檢查索引值。
- 如果我們確保能正確使用,可以省掉一些安全檢查(包括迭代過程的異常我們也可以省略),但這通常是不好的設計,為了更高一點的效率而采用危險的做法不可取,當然也沒人硬性規定你不能這么做,為了更高的性能,完全可以自己實現一個ArrayList。
而更多的詳細的分析和介紹,請移步:Java之ArrayList源碼解讀(JDK 1.8),里面會對其實現有更為詳細的介紹。
LinkedList
LinkedList相對ArrayList添加了更多的方法,這些方法也很好的為使用LinkedList實現隊列、雙向隊列、棧等提供了很好的支持。
LinkedList底層使用雙向鏈表實現,一個節點持有前一個節點的引用、需要保存的元素的引用和后一節點的引用。空的LinkedList中first和last都為null,只有一個元素(節點)的LinkedList中first->next==last、last.prev==first。
所有操作的底層就是對鏈表的操作,對鏈表實現了各種增刪改查的方法,然后對外提供了更多操作行為,如隊列的操作,棧的操作和普通List的操作,這些操作可能實際的實現是相同的,但是對外提供不同的接口以展示更多的行為。
LinkedList不是線程安全的,但也支持快速報錯,起機制與ArrayList時相同的(可以見上文介紹,此處不再贅述了)。
LinkedList中一些表面相同的行為的方法,結果可能是不同的,不如隊列的出隊,若隊列為空,有返回null和拋出異常之分,也有是否刪除出隊的節點之分,詳情請看源碼解讀。
LinkedList中表現為棧的操作的實現,是從鏈表的前端操作的,好處顯而易見,當我們需要它表現為棧的特性時,通過迭代器返回的順序也應為出棧的順序。
關於性能:
- LinkedList因為使用鏈表實現,所以在列表中間插入刪除元素時非常快速的,相反,隨機訪問時,必須從首節點(或尾節點,索引小於列表長度一半時從隊首開始遍歷,反之從隊尾)開始逐個遍歷,相對於ArrayList會更慢。查找元素的時間復雜度是相同的,實際上ArrayList可能會略快一點。
- LinkedList提供了比較豐富的操作,支持多種數據結構,使用起來固然方便。不過如果要追求完滿,比如僅僅需要一個普通的隊列,采用LinkedList總感覺又太“奢侈”了,我們完全可以自己基於單項鏈表來實現,然后值提供我們需要的方法。當然,這在時間上也是成本!
總之,LinkedList是一個很強大的工具。
雖然LinkedList提供了很多操作,諸如棧和隊列的,但有時候我們任然希望一個功能更專一的棧或隊列,下面就演示以代理模式通過LinkedList實現的棧和隊列:
- //LinkedList的使用
- public class TestUse {
- public static void main(String[] args) {
- //棧
- Stack<Integer> stack = new Stack<Integer>();
- for(int i=0;i<10;i++)
- stack.push(i);
- System.out.println(stack.peek());
- System.out.println(stack.peek());
- System.out.println(stack.pop());
- System.out.println(stack.pop());
- System.out.println(stack.pop());
- Iterator<Integer> iterator = stack.iterator();
- while(iterator.hasNext())
- System.out.print(iterator.next());
- System.out.println();
- //隊列
- Queue<Integer> queue = new Queue<Integer>();
- for(int i=0;i<10;i++)
- queue.enqueue(i);
- System.out.println(queue.peek());
- System.out.println(queue.peek());
- System.out.println(queue.dequeue());
- System.out.println(queue.dequeue());
- System.out.println(queue.dequeue());
- iterator = queue.iterator();
- while(iterator.hasNext())
- System.out.print(iterator.next());
- }
- }
- //很使用使用代理模式利用LinkedList實現一個棧
- class Stack<T> implements Iterable<T>{
- private LinkedList<T> stack = new LinkedList<T>();
- public T pop(){//出棧,會刪除棧頂元素
- return stack.poll();
- }
- public T peek(){//出棧,但不刪除棧頂元素
- return stack.peek();
- }
- public void push(T t){//入棧
- stack.push(t);
- }
- @Override
- public Iterator<T> iterator() {
- return stack.iterator();
- }
- }
- //很使用使用代理模式利用LinkedList實現一個隊列
- class Queue<T> implements Iterable<T>{
- private LinkedList<T> queue = new LinkedList<T>();
- public void enqueue(T t){
- queue.offer(t);
- }
- public T dequeue(){
- return queue.poll();
- }
- public T peek(){
- return queue.peek();
- }
- @Override
- public Iterator<T> iterator() {
- return queue.iterator();
- }
- }
而更多的詳細的分析和介紹,請移步:Java之LinkedList源碼解讀(JDK 1.8),里面會對其實現有更為詳細的介紹。
1.2、Set
存入Set中的每個元素都必須是唯一的,因為Set不保存重復的元素,加入Set的元素必須定義equals()方法以確保對象的唯一性。
Set與Collection有完全一樣的接口,所以就沒有任何額外的功能。實際上,Set也就是Collection,只是行為不同。
Set不保證維護元素的次序。
下面介紹其中的一些實現。
HashSet
為快速查找兒設計的Set。存入HashSet的元素必須定義HashCose()方法。
如果沒有其他限制,這應該是我們的首選,因為它對速度做了優化。
TreeSet
保持次序的Set,底層為樹結構。使用它可以從Set中提取有序的序列。元素必須實現Comparable()接口。
LinkedHashSet
具有HashSet的查詢速度,且內部使用鏈表維護元素的順序(插入的順序)。於是在使用迭代器遍歷的時候,結果會按元素插入的次序顯示。元素也必須定義hashCode()方法。
SortedSet
比如TreeSet就是其中一種實現方式。
SortedSet中的元素可以保證處於排序狀態(按對象的比較函數排序,而不是插入的順序)。
1.3、Map
Map可以將對象映射到其他對象。
映射表(也稱為關聯數組)的基本思想是它維護的是鍵-值(對)關聯,因此可以使用鍵來查找值。
標准的JAVA類庫中包含以下實現:HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap等。他們的基類接口是一樣的(Map),但是行為特性各不相同,比如效率、鍵值對的保持及呈現次序、對象的保持周期、映射表如何在多線程程序中工作和判定“鍵”等價的策略等。
Map中key的使用與對Set中的元素要求一樣,任何鍵都必須有一個eauals()方法。如果鍵被用於散列Map,則還必須具有恰當的hashCode()方法。如果鍵被用於TreeMap,它必須實現Compareble。
性能
在映射表中使用get()方法做線性搜索時,執行速度會很慢,兒使用HashMap可以提高速度。HashMap使用散列碼,HashMap使用hashCode()進行快速查詢。如果對速度還有更高的需求,可以自己創建Map的實現,並移除泛型支持等。
HashMap
基於散列表實現(取代HashTable)。插入和查詢“鍵值對”的開銷是固定的。可以通過構造器設置容量和負載因子,以調整容器性能。
如果沒有其他限制,這應該是我們首選的實現方式,因為它對速度做了優化。其他實現有更多增強型特性,所以速度上回有取舍。
LinkedHashMap
類似於HashMap,但是迭代訪問時,取得的“鍵值對”的順序就是插入的順序,或者是最近最少使用(LRU)的次序。比HashMap稍慢。在使用迭代器訪問時,有更快的速度,因為內部使用鏈表維護次序。
為提高速度,LinkedHashMap散列化所有的元素,但是在遍歷鍵值對時,卻又以元素的插入順序返回鍵值對。
可以在構造器中設定LinkedHashMap,使之采用LRU(最近最少使用)算法,所以沒有被訪問的元素就會出現在隊列的前面。對於需要定期清理元素節約空間的程序來說,這個功能就能使程序和容易實現。
TreeMap
基於紅黑樹的實現。查看“鍵”或“值”時,他們會被排序(具體次序有比較函數決定)。TreeMap特點在於,所得到的結果都是經過排序的。TreeMap是唯一帶有subMap()方法的Map,可以返回一個子樹。
WeakHashMap
弱鍵映射,允許釋放映射所指向的對象。是為了一些特殊的問題設計的。如果映射之外沒有引用指向某個“鍵”,這個鍵就可以被垃圾回收器回收。
ConcurrentHashMap
一種線程安全的Map,不涉及同步鎖。
IdentityHashMap
使用==代替equals()對鍵進行比較的散列映射。也是為解決特殊問題設計的。
SortedMap
TreeMap是目前SortedMap的一種實現方式,可以確保鍵處於排序狀態。
1.4、QUEUE
1.5、迭代器
迭代器
一種設計模式,在這里是一個對象。功能是遍歷並選擇序列中的對象。java中的迭代器只能單向移動:
- 1、使用方法iterator使容器返回一個Iterator,Iterator會准備好返回序列的第一個元素。
- 2、使用next()獲得序列的下一個元素
- 3、使用hasNext()判斷是否還有元素
- 4、使用remove()將迭代器新近返回的元素刪除
1.6、散列與散列碼
標准類庫中的類可以被用作HashMap的鍵,因為他們具備了鍵所需的全部性質。
當我們自己創建用作HashMap的鍵的類時,就必須在其中添加必要的方法。
我們創建的類,默認會繼承Object類,Object的hashCode()方法生成的散列碼默認使用的是對象的地址來計算散列碼。所以,試過僅僅是編寫一個普通類,是不能用於HashMap的鍵的,首先要編寫恰當的hashCode方法的覆蓋版本。而僅僅是這樣,依然不能工作,我們還需要編寫eauals()方法,它也是Object的一部分。HashMap使用equals()方法判斷當前的鍵是否與表中存在的鍵相同,默認的equals()方法同樣是比較的對象的地址。
正確的equals()方法必須滿足下面的5個條件:
- 自反性:對任意的x,x.equals(x)一定成立
- 對稱性:
- 傳遞性:
- 一致性:
- 對任何不為null的x,x.equals(null)一定返回false。
如果不為鍵覆蓋hashCode()和equals()方法,在使用散列的數據結構時就不能正確處理這個鍵。
hashCode()
首先,使用散列的目的在於:想要使用一個對象來查找另外一個對象。但是使用TreeMap或者自己實現Map也能達到目的(比如使用兩個ArrayList分別存放key和值就能很容易的實現一個Map)。
所以創建一個新的Map並不困難,但是,我們設計的時候,首先就應該考慮到速度問題,散列的價值就在於速度:散列可以得到一個非常快速的查詢速度。
查詢的速度主要取決於key的查找,其中一種方法就是保持鍵的排序狀態,然后使用二叉搜索樹進行查詢。
但是散列是一種更優的方式,它將key存儲在沒某個地方以便於能過快速找到。存儲一組元素(key的相關信息)最快的是數組,但是數組不能調整容量,這將是一個很大的限制。然后,可以如下設計:數組並不保存key本身,我們通過對象生成一個數字,將其作為數組的下標。兒這個數字就是散列碼,由定義在Object或者由其子類覆蓋的hashCode()方法生成(散列函數)。而由於數組的容量被固定了,不同的鍵也可能產生相同的下標(即產生沖突),所以數組有多大就不重要了,只要任何鍵都能在數組中找到它的位置。
於是查詢一個值的過程首先就是計算散列碼,然后使用散列碼查詢數組。
如果有沒有沖突的查詢過程,那就是有一個完美的散列函數,但這畢竟是特例。通常,沖突由外部鏈接處理:數組不直接保存值,二十保存值的list。然后使用eauals()方法對list中的值進行線性查找(其中較慢的一部分)。如果散列函數比較好,數組的每個位置都只有少量的值,因此查找的時候不是針對整個list,而是快速跳到數組的某個位置,只對很少的元素進行比較。所以HashMap就會如此快。
下面我們來簡單實現一個散列Map:
由於散列表中的槽位通常稱為桶位,因此我們將表示實際散列表的數組命名為為bucket。為師散列分布均勻,同的數量通常使用質數(其實,質數並不是散列桶的理想同期,java的散列函數都使用2的整數次方,除法和求余數是最慢的操作,詳情請查閱更多相關資料)。
經過以上的介紹,這里就探討如何編寫自己的hashCode()方法。
設計hashCode()應該保證,無論何時調用同一個對象的hashCode()都能生成相同的值。而且也不應該使用具有唯一性的對象信息,如this的值,這可能會產生一個很糟糕的hashCode()。
《effective java programming language guide》中給出了一些hashCode()的基本指導:
1、給int變量result賦予某個非零值常量
2、為對象內每個有意義的域f(每個可以做equals()操作的域)計算出一個int散列碼c
3、合並計算得到的散列碼
4、返回result
5、檢查hashCode()最后生成的結果,確保相同的對象有相同的散列碼。
根據以上指導實現的hashCode()的一個例子:
1.7、小結
Java中提供的其實只有4中容器:Map、List、Set和Queue。
Java提供了大量的持有對象方式:
1、數組將數字與對象聯系起來。它保存類型明確的對象,查詢對象是,不需要對結果做類型轉換。他可以是多維的,可以保存基本類型的數據。但是,數組一旦生成,其容量就不能改變。
2、Collection保存單一的元素,而Map保存相關聯的鍵值對。有了Java的泛型,就可以指定容器中存放的對象類型,因此就不會將錯誤類型的對象防止到容器中,而且在容器中獲取元素時,不必進行類型轉換。各種Collection和Map都可以在向其中添加更多的元素時,自動調整其尺寸。容器不能持有基本類型,但是自動包裝機制會仔細地執行基本類型到容器中所持有的包裝器類型之間的雙向轉換。
3、像數組一樣,List也建立數字索引與對象的關聯,因此,數組和List都是排序好的容器。List能夠自動擴充容量。
4、如果要進行大量的隨機訪問,就是用ArrayList。如果要經常從表中間插入或刪除元素,則應該使用LinkedList。
5、各種Queue以及棧的行為,有LinkedList提供支持。
6、Map是一種將對象與對象相關聯的設計。HashMap設計用來快速訪問TreeMap保存“鍵”始終處於排序狀態(沒有HashMap快);LinkedHashMap保持元素插入的順序,但是也通過散列提供了快速訪問能力。
7、Set不接受重復元素。HashSet提供最快的查詢速度,TreeSet保持元素處於排序狀態。LinkedHashSet以出入順序保存元素。
2、接口不同實現的選擇
Java中實際上只有4中容器:Map、Set、List、Queue。但是每種接口都有不止一個實現版本,所以合理的選擇具體的實現。
每種不同的實現由各自的特征,優缺點。比如Hashtable、Vector、Stack的特征就是他們已經過時o(^▽^)o(目的只是為了支持老的程序)。
我們現在具體實現時,首要就是考慮我們要實現什么樣的數據結構。
比如:ArrayList和LinkedList都實現了List接口,兩者基本的List操作都是相同的。但是ArrayList底層有數組支持;而LinkedList由雙向鏈表實現,每個對象除了包含數據本身的同時還包含只想前后兩個元素的引用。因此,如果需要經常在表中插入或是刪除數據的話,LinkedList更佳合適;否則,應該使用ArrayList達到更多的速度。
同樣的,Set可被實現為TreeSet,HashSet或LinkedHashSet。每一種都有不同的行為:HashSet最常用,以為其查詢速度最快;LinkedHashSet保持元素插入的順序;TreeSet給予TreeMap,生成一個總是處於排序狀態的Set。根據不同的需求以選擇不同的實現。
下面會介紹一個性能測試框架(程序來自Java編程思想)。
2.1、性能測試框架
3、Java容器框架圖
來看一下相關java容器相關的結構:
Sorry,以上是JDK1.8 前的一張圖,本人也基於JDK1.8 繪制了一張,下文也是基於這張圖來寫的:
4、源碼分析索引
基於JDK 1.8 。
4.1、Iterable
我們從最上面開始看,根為Iterable,Iterable是一個接口,看下源碼:
- public interface Iterable<T> {
- Iterator<T> iterator();
- default void forEach(Consumer<? super T> action) {
- Objects.requireNonNull(action);
- for (T t : this) {
- action.accept(t);
- }
- }
- default Spliterator<T> spliterator() {
- return Spliterators.spliteratorUnknownSize(iterator(), 0);
- }
- }
Iterator<T> iterator():返回一個在一組 T 類型的元素上進行迭代的迭代器。
實現此接口允許對象使用"for-each 循環"語句(在Java8發布之際,有件事情就顯得非常重要,即在不破壞java現有實現架構的情況下能往接口里增加新方法。引入Default方法到Java8,正是為了這個目的:優化接口的同時,避免跟現有實現架構的兼容問題)。
再看下Iterator:
- public interface Iterator<E> {
- E next();
- default void remove() {
- throw new UnsupportedOperationException("remove");
- }
- default void forEachRemaining(Consumer<? super E> action) {
- Objects.requireNonNull(action);
- while (hasNext())
- action.accept(next());
- }
- }
迭代其實我們可以簡單地理解為遍歷,是一個標准化遍歷各類容器里面的所有對象的方法類,它是一個很典型的設計模式。Iterator模式是用於遍歷集合類的標准訪問方法。它可以把訪問邏輯從不同類型的集合類中抽象出來,從而避免向客戶端暴露集合的內部結構。
ListIterator
ListIterator為專為LIst設計的增強版本迭代器,可以雙向移動。
- package java.util;
- public interface ListIterator<E> extends Iterator<E> {
- boolean hasNext();
- E next();
- boolean hasPrevious();
- E previous();
- int nextIndex();
- int previousIndex();
- void remove();
- void set(E e);
- void add(E e);
- }
可以看到hasPrevious()和previous()就是判斷是否前面還有值和得到前一項。
2.2、Collection
Collection是直接繼承Iterable的一個接口,所以Collection的導出類都應該事先迭代器的,並都能使用foreach遍歷。
- public interface Collection<E> extends Iterable<E> {
- int size();
- boolean isEmpty();
- boolean contains(Object o);
- Iterator<E> iterator();
- Object[] toArray();
- <T> T[] toArray(T[] a);
- boolean add(E e);
- boolean remove(Object o);
- boolean containsAll(Collection<?> c);
- boolean addAll(Collection<? extends E> c);
- boolean removeAll(Collection<?> c);
- default boolean removeIf(Predicate<? super E> filter) {
- Objects.requireNonNull(filter);
- boolean removed = false;
- final Iterator<E> each = iterator();
- while (each.hasNext()) {
- if (filter.test(each.next())) {
- each.remove();
- removed = true;
- }
- }
- return removed;
- }
- boolean retainAll(Collection<?> c);
- boolean equals(Object o);
- int hashCode();
- @Override
- default Spliterator<E> spliterator() {
- return Spliterators.spliterator(this, 0);
- }
- default Stream<E> stream() {
- return StreamSupport.stream(spliterator(), false);
- }
- default Stream<E> parallelStream() {
- return StreamSupport.stream(spliterator(), true);
- }
- }
其實通過函數名我們已經知道這些方法都是干什么的了,不過具體是如何操作的,返回代表什么?我們得往下看具體實現才行。
2.3、List
- public interface List<E> extends Collection<E> {
- int size();
- boolean isEmpty();
- boolean contains(Object o);
- Iterator<E> iterator();
- Object[] toArray();
- <T> T[] toArray(T[] a);
- boolean add(E e);
- boolean remove(Object o);
- boolean containsAll(Collection<?> c);
- boolean addAll(Collection<? extends E> c);
- boolean addAll(int index, Collection<? extends E> c);
- boolean removeAll(Collection<?> c);
- boolean retainAll(Collection<?> c);
- default void replaceAll(UnaryOperator<E> operator) {
- Objects.requireNonNull(operator);
- final ListIterator<E> li = this.listIterator();
- while (li.hasNext()) {
- li.set(operator.apply(li.next()));
- }
- }
- default void sort(Comparator<? super E> c) {
- Object[] a = this.toArray();
- Arrays.sort(a, (Comparator) c);
- ListIterator<E> i = this.listIterator();
- for (Object e : a) {
- i.next();
- i.set((E) e);
- }
- }
- void clear();
- boolean equals(Object o);
- int hashCode();
- E get(int index);
- E set(int index, E element);
- void add(int index, E element);
- E remove(int index);
- int indexOf(Object o);
- int lastIndexOf(Object o);
- ListIterator<E> listIterator();
- ListIterator<E> listIterator(int index);
- List<E> subList(int fromIndex, int toIndex);
- @Override
- default Spliterator<E> spliterator() {
- return Spliterators.spliterator(this, Spliterator.ORDERED);
- }
- }
List可以像數組一樣持有對象,但是大小可以自動調整,並且提供了更多的操作。我們來看一下有哪些實現:
其中有些事接口,有些事抽象類,這里主要看兩個實現類:LinkedList和ArrayList。
ArrayList
限於篇幅,此處將源碼解讀放到另外一篇文章:ArrayList源碼解讀
LinkedList
2.4、Set
2.5、Queue