Java——容器類庫框架淺析
前言
通常,我們總是在程式執行過程中才獲得一些條件去建立物件,這些動態建立的 物件就需要使用一些方式去儲存 。我們可以使用陣列去儲存,但是需要注意陣列的尺寸一旦定義便不可修改,而我們並不知道程式在執行過程中會產生多少物件,於是陣列的尺寸便成了限制。 Java實用類庫還提供了一套的容器類來解決這個問題,基本型別為:List 、Set、Queue和Map 。這些物件型別也稱為 集合類 ,但是由於Java類庫使用了Collection這個名字來指代該類庫中的一個特殊子集,所以使用術語“容器”來稱呼它們。下面將簡單介紹Java容器類庫的基本概念和功能、每個容器的不同版本實現和和從整體分析容器之間的聯絡。
Java容器類庫概覽
Java容器類庫的主要作用是“儲存物件”,我們將其劃分成以下兩個不同的概念:
Collection
一個獨立的元素 序列 (一種存放一組物件的方式),這些元素都服從一條或多條規則。List必須按照插入的順序儲存元素;Set中不能有重複的元素;Queue按排隊規則來確定物件產生的順序。
Collection是一個高度抽象的容器介面,其中包含了容器的基本操作和屬性。
Map
一組成對的“鍵值對”物件,允許你使用鍵來查詢值。
框架類圖中還包含了許多 Abstract類 ,主要方便於我們建立容器的例項,Abstract類中已基本實現了介面中的方法,我們只需要選擇我們需要的方法進行覆蓋即可。
Iterator
我們再來看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: 0Cat: 3Cat: 6 Cat: 1Cat: 4Cat: 7 Cat: 2Cat: 8Cat: 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.nextIndexpreviousIndex 正向遍歷: Cat: 010 Cat: 121 Cat: 232 Cat: 343 Cat: 454 當迭代器遊標處於最後一個元素末尾時: Cat: 054 Cat: 154 Cat: 254 Cat: 354 Cat: 454 反向遍歷 Cat: 443 Cat: 332 Cat: 221 Cat: 110 Cat: 00-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): https://www.cnblogs.com/skywang12345/p/3323085.html
《Java程式設計思想》第四版