Java——容器類庫框架淺析

前言

一般,咱們老是在程序運行過程當中纔得到一些條件去建立對象,這些動態建立的對象就須要使用一些方式去保存。咱們可使用數組去存儲,可是須要注意數組的尺寸一旦定義便不可修改,而咱們並不知道程序在運行過程當中會產生多少對象,因而數組的尺寸便成了限制。Java實用類庫還提供了一套的容器類來解決這個問題,基本類型爲:List 、Set、Queue和Map。這些對象類型也稱爲集合類,可是因爲Java類庫使用了Collection這個名字來指代該類庫中的一個特殊子集,因此使用術語「容器」來稱呼它們。下面將簡單介紹Java容器類庫的基本概念和功能、每一個容器的不一樣版本實現和和從總體分析容器之間的聯繫。html

Java容器類庫概覽

Java容器類的框架圖

Java容器類庫的主要做用是「保存對象」,咱們將其劃分紅如下兩個不一樣的概念:java

Collection算法

一個獨立的元素序列(一種存放一組對象的方式),這些元素都服從一條或多條規則。List必須按照插入的順序保存元素;Set中不能有重複的元素;Queue按排隊規則來肯定對象產生的順序。編程

Collection是一個高度抽象的容器接口,其中包含了容器的基本操做和屬性。數組

Map安全

一組成對的「鍵值對」對象,容許你使用鍵來查找值。數據結構

框架類圖中還包含了許多Abstract類,主要方便於咱們建立容器的實例,Abstract類中已基本實現了接口中的方法,咱們只須要選擇咱們須要的方法進行覆蓋便可。併發

Iteratorapp

咱們再來看Iterator,咱們一般是使用Iterator迭代器來遍歷容器。上圖存在的Collection依賴於Iterator是指:實現Collection須要實現iterator()函數,能夠返回一個Iterator對象。ListIterator是專門用於遍歷List的迭代器。框架

工具類Arrays和Collections爲容器添加元素

java.util包中的Arrays和Collections類中包含了不少的實用方法。Arrays類中包含操做數組的各類方法,還包含一個靜態的Arrays.asList()方法接受一個數組或是用逗號分隔的元素列表,將其轉換成一個列表對象。Collection類包含對集合操做的各類方法。咱們也可使用Collections.addAll()想容器中添加一組元素。Collections.addAll()接受一個Collection對象以及一個數組或者用逗號分隔的元素列表,將元素添加到Collection對象中。

Arrays.asList()的底層實現是一個數組,即便用Arrays.asList()生成的List的尺寸是不能夠修改的(添加或刪除元素),不然將會拋出UnsupportedOperationException異常。

List

List接口繼承自Collection接口,用於Collection中的全部方法,在Collection的基礎上也添加了許多方法,使得能夠在List中插入和刪除元素。List有兩種基本的實現:ArrayList和LinkedList

  • 基本的ArrayList,它適合於隨機訪問元素,可是在插入和刪除元素時就比較慢
  • LinkedList適合於在元素插入和刪除較頻繁時使用,隨機訪問的速度比較慢

Set

Set中不保存重複的元素,含義同數學概念上的集合。 Set經常使用於測試歸屬性,即查詢某個元素是否在某個Set中。正由於如此查找也就成了Set中重要的操做。一般會選擇HashSet的實現,它對快速查找進行了優化。Set也有多種不一樣的實現,不一樣的Set實現不只具備不一樣的行爲,並且它們對於能夠在特定的Set中防止元素的類型也有不一樣的要求。

  • Set(interface)

    存入Set的每一個元素必須是惟一對的。加入Set的元素必須定義equals()方法以確保對象的惟一性。Set接口不保證維護元素的次序。

  • HashSet

    爲快速查找而設計的Set。存入HashSet的元素必須定義hashCode()。使用HashMap實現。

  • TreeSet

    保持次序的Set,底層爲樹結構。使用它能夠從Set中提取有序的序列。元素必須實現Comparable接口。使用TreeSet實現。

  • LinkedHashSet

    具備HashSet的查詢速度,且內部使用鏈表維護元素的順序(插入的次序)。在使用迭代器遍歷該Set時,結果會按照元素插入的次序顯示。元素也必須定義hashCode()方法

Map

Map有如下特色:

  • Map是將鍵映射到值的鍵值對(key-value)接口

  • 映射中不能包含重複的鍵,每一個鍵最多能夠映射到一個值,可是一個值能夠被多個鍵映射

  • Map提供了三個Set視圖供咱們訪問:鍵的Set、值的Set和鍵值對的Set

  • 映射的順序定義爲訪問的映射Set上的迭代器返回元素的順序。TreeMa類,能夠對映射的順序作出特定保證;其餘的,則不能保證

  • 可變對象做爲映射鍵須要很是當心

  • Map的實現類應該提供兩個「標準「構造函數

    第一個,void(無參數)構造方法,用於建立空映射

    第二個,帶有單個 Map 類型參數的構造方法,用於建立一個與其參數具備相同鍵-值映射關係的新映射。帶有單個 Map 類型參數的構造方法,用於建立一個與其參數具備相同鍵-值映射關係的新映射

Map的幾種基本實現:

  • HashMap

    Map是基於散列表的實現(取代了HashTable)。HashMap使用散列碼(對象hashCode()生成的值)來進行快速搜索。

  • LinkedHashMap

    相似於HashMap,可是迭代的時候,取得鍵值對的順序是起插入的順序,或者是最近最少使用(LRU)的次序。只比HashMap慢一點,而迭代訪問的時候更快,由於使用鏈表維護了內部次序。

  • TreeMap

    基於紅黑樹的實現。查看「鍵」或者「鍵值對」時,它們會被排序(次序由Comparable或Comparator決定)。TreeMap的特色在於所獲得的結果是通過排序的。TreeMap是惟一的帶有subMap()的Map,能夠返回一個子樹。

  • WeakHashMap

    弱鍵(weak key)映射,容許釋放映射所指向的對象,這是爲了解決某類特殊問題而設計的。若是映射以外沒有引用指向某個「鍵」,則此「鍵」能夠被垃圾回收。

  • ConcurrentHashMap

    一種線程安全的Map,不涉及同步加鎖。在併發中還會介紹。

Stack和Queue

Stack是一個先進後出(LIFO)的容器。往盒子中放書,先放進去的最後纔拿得出來,最後放進去的第一個就能夠取出,這種模型就是棧(Stack)能夠描述的。LinkedList中有能夠實現棧全部功能的方法,有時也能夠直接將LinkedList做爲棧使用。

隊列是一個典型的先進先出(FIFO)的容器。事物放進容器的順序和取出的順序是相同的(優先級隊列根據事物優先級出隊事物)。隊列常被當作一種可靠的將對象從程序的某個區域傳輸到另外一個區域的途徑。隊列在併發編程中特別重要。一樣,LinkedList也提供了方法支持隊列的行爲,而且它實現了Queue接口。

優先級隊列PriorityQueue

先進先出描述了典型的隊列規則。隊列規則是指在給定一組隊列的元素狀況下,肯定下一個彈出隊列的元素的規則。優先級隊列聲明的下一個彈出的元素是最須要的元素(具備最高優先級的元素)。

咱們能夠在PriorityQueue上調用offer()方法來插入一個對象,這個對象就會在隊列中被排序,默認排序爲天然排序,即按插入的前後進行排序,可是咱們能夠經過提供本身的Comparator來修改這個排序。當調用peek()、poll()和remove()方法時,將獲取隊列優先級最高的元素。

優先級隊列算法實現的數據結構一般是一個堆。

迭代器

對於訪問容器而言,有沒有一種方式使得同一份遍歷的代碼能夠適用於不一樣類型的容器?要實現這樣的目的就可使用迭代器。使用迭代器對象,遍歷並選擇序列中的對象,而客戶端沒必要知道或關心該序列底層的結構。Java中對迭代器有一些限制,好比Java的Iterator只能單向移動,這個Iterator只能用來:

  • 使用next()方法得到序列的下一個元素
  • 使用hasNext()方法檢查序列中是否還有元素
  • 使用remove()方法將迭代器新近返回的元素刪除,意味着在調用remove()以前必須先調用next()

API中的Iterator接口中方法如上,實現Iterator對象須要實現hashNext()方法和next()方法,remove方法是一個可選操做。forEachRemaining是Java 1.8(Java SE8)中加入的方法,用於Lambda表達式。

舉一個簡單的使用迭代器訪問容器的例子:

class Cat{
    private static int counter = 0;
    private int id = counter++;
    @Override
    public String toString() {
        return "Cat: " + id;
    }
}

public class IteratorAccessContainer {
    //不包含任何容器類型信息的遍歷容器方法
    public static void showElement(Iterator<Cat> it) {
        while (it.hasNext()) {      //hasNext()檢查序列中是否還有元素
            Cat cat = it.next();    //next()返回序列中的元素
            System.out.print(cat + "\t");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        ArrayList<Cat> cats1 = new ArrayList<Cat>();
        LinkedList<Cat> cats2 = new LinkedList<>(); //能夠省略類型參數 編譯器可自動推斷出
        HashSet<Cat> cats3 = new HashSet<>();
        for(int i=0;i<3; i++) {
            cats1.add(new Cat());
            cats2.add(new Cat());
            cats3.add(new Cat());
        }
        showElement(cats1.iterator());
        showElement(cats2.iterator());
        showElement(cats3.iterator());
    }
}
/*
output:
Cat: 0  Cat: 3  Cat: 6  
Cat: 1  Cat: 4  Cat: 7  
Cat: 2  Cat: 8  Cat: 5  
*/

showElement()方法不包含任何有關它遍歷的序列類型信息,這就展現了Iterator的好處:可以將遍歷序列的操做與序列底層結構分離。也能夠說,迭代器統一了對容器的訪問方式

從容器框架圖中咱們能夠看出,Collection是描述全部序列容器的共性的根接口。可是在C++中,標準的C++類庫中沒有其餘容器的任何公共基類,容器之間的共性都是經過迭代器達成的。在Java中,則將兩種方法綁定到了一塊兒,實現Collection的同時也要實現iterator()方法(返回該容器的迭代器)。

ListIterator

ListIterator是一個更增強大的Iterator子類型,可是它只能用於各類List的訪問。Iterator只能前向移動,但ListIterator容許咱們能夠先後移動。它還能夠產生相對於迭代器在列表中指向當前位置的前一個和後一個索引,而且可使用set()方法替換它訪問過的最後一個元素。remove()方法能夠刪除它訪問過的最後一個元素。須要注意,這兩處的最後一個元素只的都是調用next()或者previous返回的元素,也就意味着調用set()、remove()這兩個方法以前,要先調用next()或者previous()。

須要注意ListIterator在序列中的遊標位置與Iterator不一樣,Iterator的遊標位置始終位於調用previous()將返回的元素和調用next()將返回的元素之間。長度爲n的列表的迭代器的遊標位置有n+1個。

使用ListIterator對列表進行正向和返回迭代,以及使用set()替換列表元素的例子:

public class ListIteration {
    public static void main(String[] args) {
        List<Cat> catList = new ArrayList<>();
        for(int i=0; i<5; i++) {
            catList.add(new Cat());
        }
        
        ListIterator<Cat> it = catList.listIterator();
        System.out.println("CatNo.\t nextIndex\t previousIndex");
        
        //正向遍歷
        System.out.println("正向遍歷:");
        while (it.hasNext()) {
            Cat cat = it.next();
            System.out.println(cat+"\t\t"+it.nextIndex()+"\t\t"+it.previousIndex());
        }
        System.out.println();
        
        System.out.println("當迭代器遊標處於最後一個元素末尾時:");
        ListIterator<Cat> it2 = catList.listIterator();
        while (it2.hasNext()) {
            Cat cat = it2.next();
            System.out.println(cat+"\t\t"+it.nextIndex()+"\t\t"+it.previousIndex());
        }
        System.out.println();
        
        //反向遍歷
        System.out.println("反向遍歷");
        while(it.hasPrevious()) {
            Cat cat = it.previous();
            System.out.println(cat+"\t\t"+it.nextIndex()+"\t\t"+it.previousIndex());
        }
        System.out.println();
        
        //產生指定遊標位置的迭代器 從第二個位置開始向前替換列表中的Cat對象
        System.out.println("從第二個位置開始向前替換列表中的Cat對象");
        it = catList.listIterator(2);
        while(it.hasNext()) {
            it.next();
            it.set(new Cat());
        }
        System.out.println(catList);
    }
}
/*
CatNo.   nextIndex   previousIndex
正向遍歷:
Cat: 0      1       0
Cat: 1      2       1
Cat: 2      3       2
Cat: 3      4       3
Cat: 4      5       4

當迭代器遊標處於最後一個元素末尾時:
Cat: 0      5       4
Cat: 1      5       4
Cat: 2      5       4
Cat: 3      5       4
Cat: 4      5       4

反向遍歷
Cat: 4      4       3
Cat: 3      3       2
Cat: 2      2       1
Cat: 1      1       0
Cat: 0      0       -1

從第二個位置開始向前替換列表中的Cat對象
[Cat: 0, Cat: 1, Cat: 5, Cat: 6, Cat: 7]
*/

foreach與迭代器

foreach語法不只能夠用在數組,也能夠用在任何Collection對象。之因此能夠用在Collection對象,是由於Java SE5引入了Iterable接口,該接口包含一個可以產生Iterator的iterator()方法,而且Iterable接口被foreach用來在序列中移動。所以,若是建立了任何實現Iterable的類,均可以將它用於foreach當中。須要注意,數組雖然可使用foreach語法遍歷,但不意味着數組是Iterable的

實現一個可迭代的類,使用foreach方法遍歷

public class IterableClass implements Iterable<String>{
    private String[] words = ("This is happy day.").split(" ");
    @Override
    public Iterator<String> iterator() {
        return new Iterator<String>() {
            private int index = 0;
            //判斷是否存在下一個元素
            public boolean hasNext() {
                return index < words.length;
            }
            //返回下一個元素
            public String next() {
                return words[index++];
            }
            public void remove() {  //remove能夠不用實現
                throw new UnsupportedOperationException();
            }
        };
    }
    
    public static void main(String[] args) {
        //foreach語法遍歷實現了Iterable接口的類
        for(String s : new IterableClass()) {
            System.out.println(s);
        }
    }
}
/*
This
is
happy
day.
*/

小結

對Java容器類庫作了大體的介紹,具體的容器使用方法以及實現會在後面的博客中繼續介紹。本文重點介紹了Iterator,它統一了對容器的訪問方式,可是仍有一點心存疑惑:foreach語法能夠遍歷容器是由於容器實現了Iterable的緣由,可是也能夠遍歷數組,數組並非Iterable的。那麼foreach能夠遍歷數組的依據是什麼呢?這個問題暫時尚未看到合適的解答,各位看官如有想法可留言告知,感激涕零!

參考:

Java 集合系列目錄(Catedory): http://www.javashuo.com/article/p-tjlggnrm-hq.html

《Java編程思想》第四版

相關文章
相關標籤/搜索