【Java】容器類庫框架淺析與迭代器


前言

通常,我們總是在程序運行過程中才獲得一些條件去創建對象,這些動態創建的對象就需要使用一些方式去保存。我們可以使用數組去存儲,但是需要注意數組的尺寸一旦定義便不可修改,而我們並不知道程序在運行過程中會產生多少對象,於是數組的尺寸便成了限制。Java實用類庫還提供了一套的容器類來解決這個問題,可大致分為:List 、Set、Queue和Map。這些對象類型也稱為集合類,但是由於Java類庫使用了Collection這個名字來指代該類庫中的一個特殊子集,所以使用術語“容器”來稱呼它們。下面將簡單介紹Java容器類庫的基本概念和功能、每個容器的不同版本實現和和從整體分析容器之間的聯系。

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,底層調用HashMap實現。使用它可以從Set中提取有序的序列。元素必須實現Comparable接口

  • LinkedHashSet

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

Map

Map有以下特點:

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

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

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

  • 映射的順序定義為訪問的映射Set上的迭代器返回元素的順序。TreeMap類可以對映射的順序做出特定保證;其他的則不能保證。

  • 可變對象作為映射鍵需要非常小心。

  • 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可以遍歷數組的依據是什么呢?(猜想是JVM底層實現了某種轉換)這個問題暫時還沒有看到合適的解答,各位看官若有想法可留言告知,感激不盡!

參考:

[1] Eckel B. Java編程思想(第四版)[M]. 北京: 機械工業出版社, 2007

[2] 如果天空不死. Java 集合系列目錄(Category)[EB/OL]. /2019-03-02. https://www.cnblogs.com/skywang12345/p/3323085.html.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM