Java開發筆記(六十五)集合:HashSet和TreeSet

對於相同類型的一組數據,雖然Java已經提供了數組加以表達,可是數組的結構實在太簡單了,第一它沒法直接添加新元素,第二它只能按照線性排列,故而數組用於基本的操做倒還湊合,若要用於複雜的處理就沒法勝任了。爲此Java設計了一大類的數據類型名叫容器,它們彷彿容納物品的器皿通常,可大可小,既能隨時往裏塞入新物件,又能隨時從中取出某物件。固然,依據不一樣的用途,容器也分爲好幾類,包括集合Set、映射Map、清單List等等,本文先從最基礎的集合開始介紹。
所謂集合,指的是一羣同類彙集在一塊兒,集合的最大特色就是裏面的每一個事物都是惟一的,即便重複加入也只算同一個元素。Java給集合分配的類型名稱叫作「Set」,在使用之時還得在Set後面補充一對尖括號,裏面填集合內部元素的數據類型。好比一個字符串集合,它的完整類型寫法爲「Set<String>」,下面即是聲明字符串集合變量的代碼例子:html

		Set<String> set;

 

但是因爲Set實際上屬於接口,所以不能直接用來建立集合實例,在編程開發中,每每使用Set的兩個實現類HashSet和TreeSet。
HashSet的大名叫哈希集合,其內部採起哈希表來存儲數據;而TreeSet的大名叫作二叉集合,其內部採起二叉樹來存儲數據。儘管HashSet與TreeSet兩者的存儲結構不一樣,但它們在編碼調用時大致相似,因此接下來就以HashSet爲例,概要描述集合的基本用法。
一開始使用集合,固然也要先建立該集合的實例。建立集合實例的方式跟建立一個類的實例相同,都得調用它們的構造方法,集合實例的具體建立代碼以下所示:java

		HashSet<String> set = new HashSet<String>();

 

有了集合實例,再經過實例去調用具體的集合方法,如下是經常使用的集合方法說明:
add:把指定元素添加到集合。
remove:從集合中刪除指定元素。
contains:判斷集合是否包含指定元素。
clear:清空集合。
isEmpty:判斷集合是否爲空。
size:獲取集合的大小(即所包含元素的個數)。
從以上說明可見,這些集合方法仍是蠻基礎的,不但基礎並且通用,不光是集合會用到這些方法,連映射和清單也要用到它們,於是後面在介紹映射和清單之時,就再也不重複說明上述的基本方法了。
接着來個集合的初步運用,功能很簡單,僅僅往字符串集合添加五個字符串,而後獲取並打印該集合的大小,示例代碼以下:程序員

		HashSet<String> set = new HashSet<String>();
		set.add("hello");
		set.add("world");
		set.add("how");
		set.add("are");
		set.add("you");
		System.out.println("set.size()=" + set.size());

 

運行上面的測試代碼,可得日誌結果爲「set.size()=5」。不過這裏只得到集合大小,若想知曉集合內部到底有哪些字符串,還得依次遍歷該集合的全部元素才行。集合元素的遍歷方式主要有三種:for循環遍歷、迭代器遍歷、forEach遍歷,接下來分別進行介紹。數據庫

一、for循環遍歷
這個for循環屬於簡化的for循環,早在遍歷數組元素的時候,你們已經見識過了。廢話沒必要多說,直接看下列代碼好了:編程

		// 第一種遍歷方式:簡化的for循環一樣適用於數組和容器
		for (String hash_item : set) {
			System.out.println("hash_item=" + hash_item);
		}

 

運行以上的循環代碼,輸出了以下的日誌信息,可見該集合的五個元素全都找到了:數組

hash_item=how
hash_item=world
hash_item=are
hash_item=hello
hash_item=you

  

二、迭代器遍歷
迭代器又稱指示器,其做用相似於數據庫的遊標,以及C語言的指針。調用集合實例的iterator方法便可得到該集合的迭代器,初始的迭代器指向集合的存儲地址;它的hasNext方法用來判斷後方是否存在集合元素,假若不存在則表示到末尾了;迭代器另有next方法用於獲取下一個元素,同時迭代器移動到下一個地址。因而屢次調用集合實例的next方法,便可逐次取出該集合的每一個元素。下面是利用迭代器遍歷集合的代碼例子:ide

		// 第二種遍歷方式:利用迭代器循環遍歷集合。
		Iterator<String> iterator = set.iterator();
		while (iterator.hasNext()) { // 迭代器後方是否存在元素
			// 獲取迭代器後方的元素
			String hash_iterator = (String) iterator.next();
			System.out.println("hash_iterator=" + hash_iterator);
		}

  

三、forEach遍歷
forEach是Java8新增的容器遍歷方法,一樣適用於映射和清單。它藉助Lambda表達式可以完成最簡化的遍歷操做,僅僅一行代碼就搞定了集合元素的循環輸出功能,具體實現代碼以下所示:測試

		// 第三種遍歷方式:使用forEach方法夾帶Lambda表達式進行遍歷
		set.forEach(hash_each -> System.out.println("hash_each=" + hash_each));

 

講完了集合的三種遍歷方式,按說集合的常見用法均涉及到了,那麼爲啥集合還要分紅哈希集合與二叉集合兩類呢?這緣於集合的基本特性規定了集合裏的每一個元素是惟一的,但並未規定集合裏的元素須要按照順序排列。從前面哈希集合的遍歷結果可知,哈希集合裏面保存的各元素是無序的,由於一個數據的哈希結果是散列值,天南地北處處跑,天然沒法按照元素值進行排序。二叉集合的設計正是要解決這個順序問題,因爲二叉集合內部採起二叉樹存儲數據,每一個新加入的元素都要與原住民們比較一番,好決定這個新元素是放在某個原住民的左節點仍是右節點;所以,假若把一組字符串前後加入二叉集合,那麼每次新增元素的操做都會進行大小比較,最終獲得的二叉集合一定是有序的。this

爲了驗證二叉集合的添加操做是否符合設計原理,接下來不妨建立一個二叉集合的實例,再往其中添加多個字符串,而後遍歷打印該字符串集合的全部元素。據此從新編寫後的二叉集合演示代碼以下所示:編碼

		TreeSet<String> set = new TreeSet<String>();
		set.add("hello");
		set.add("world");
		set.add("how");
		set.add("are");
		set.add("you");
		// 第一種遍歷方式:簡化的for循環一樣適用於數組和容器
		for (String tree_item : set) {
			System.out.println("tree_item=" + tree_item);
		}

 

運行上述的演示代碼,觀察如下的日誌信息可知,這個二叉集合的遍歷結果爲按照字符串首字母升序排列:

tree_item=are
tree_item=hello
tree_item=how
tree_item=world
tree_item=you

  

須要注意的是,無論是哈希值計算,仍是二叉節點比較,都須要元素歸屬的數據類型提供計算方法或者比較方法。對於包裝類型、字符串等系統自帶的數據類型來講,Java已經在它們的源碼中實現了相關方法,因此這些數據類型容許程序員在集合中直接使用。然而若是是開發者本身定義的數據類型(新的類),就要求開發者本身來實現計算方法和比較方法了。
譬若有個自定義的手機類MobilePhone,該類的定義代碼見下:

//定義一個手機類
public class MobilePhone {
	private String brand; // 手機品牌
	private Integer price; // 手機價格

	public MobilePhone(String brand, int price) {
		this.brand = brand;
		this.price = price;
	}

	// 獲取手機品牌
	public String getBrand() {
		return this.brand;
	}

	// 獲取手機價格
	public int getPrice() {
		return this.price;
	}
}

如今給手機類分別建立對應的哈希集合與二叉集合,並對兩種集合分別添加若干手機實例,結果會發現,手機的哈希集合竟然會插入品牌與價格重複的元素!同時手機的二叉集合也變成亂序的了,由於編譯器不曉得究竟要按照品牌排序仍是按照價格排序。既然編譯器無從判斷待添加的元素是否重複,也沒法判斷新添加的元素根據哪一個字段排序,程序員就得在手機類的定義代碼中指定相關的判斷規則了。

就哈希集合的哈希值計算而言,自定義的手機類須要重寫hashCode和equals方法,其中hashCode方法計算獲得的哈希值對應於該對象的保存位置,而equals方法用來判斷該位置上的幾個元素是否徹底相等。一方面,咱們要保證品牌與價格都相同的兩個元素,它們的哈希值必須也相等;另外一方面,即便兩個元素的品牌和價格不一致,它們的哈希值也可能恰巧相等,因而還須要equals方法進一步校驗是否存在重複。按照上述要求,重寫後的hashCode和equals方法代碼示例以下:

	// hashCode方法計算出來的哈希值對應於該對象的保存位置
	@Override
	public int hashCode() {
		return brand.hashCode() + price.hashCode();
	}

	// 同一個存儲位置上可能有多個對象(哈希值剛好相等),
	// 此時系統自動調用equals方法判斷是否存在相同的對象。
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof MobilePhoneHash)) {
			return false;
		}
		MobilePhoneHash other = (MobilePhoneHash) obj;
		// 手機品牌和手機價格都相等,纔算是這兩個手機相等
		boolean equals = this.brand.equals(other.brand) && this.price.equals(other.price);
		return equals;
	}

至於二叉集合的節點大小比較,則需手機類實現接口Comparable,並具體定義該接口聲明的compareTo方法(該方法用來比較兩個元素的大小關係)。其實這裏的Comparable接口與數組排序用到的Comparator接口做用相似,都是判斷兩個對象誰大誰小。若是要求二叉集合裏面的手機元素按照價格排序,則compareTo方法主要校驗當前手機的價格與其它手機的價格。詳細的接口實現代碼以下所示:

public class MobilePhoneTree implements Comparable<MobilePhoneTree> {
	// 此處省略手機類的構造方法、成員屬性與成員方法定義

	// 二叉樹除了檢查是否相等,還要判斷前後順序。
	// 相等和前後順序的校驗結果從compareTo方法得到。
	@Override
	public int compareTo(MobilePhoneTree other) {
		if (this.price.compareTo(other.price) > 0) { // 當前價格較高
			return 1;
		} else if (this.price.compareTo(other.price) < 0) { // 當前價格較低
			return -1;
		} else {
			return this.brand.compareTo(other.brand);
		}
	}
}

通過一番折騰以後,再對新定義的兩個手機類分別對哈希集合與二叉集合開展驗證,結果應當爲:哈希集合不會插入重複的手機對象,而且二叉集合裏的各手機元素按照價格升序排列。



更多Java技術文章參見《Java開發筆記(序)章節目錄

相關文章
相關標籤/搜索