Java集合类原理详解
文章目錄
- 1 集合框架
- 1.1 集合框架概述
- 1.1.1 容器簡介
- 1.1.1 容器的分類
- 1.2 Collection
- 1.2.1 常用方法
- 1.2.2 迭代器
- 1.3 List
- 1.3.1 概述
- 1.3.2 常用方法
- 1.3.3 實現原理
- 1.4 Map
- 1.4.1 概述
- 1.4.2 常用方法
- 1.4.3 Comparable 接口
- 1.4.4 實現原理
- 1.4.5 覆寫 hashCode()
- 1.5 Set
- 1.5.1 概述
- 1.5.2 常用方法
- 1.5.3 實現原理
- 1.6 總結:集合框架中常用類比較
- 2 練習
- 3 附錄:排序
1 集合框架
1.1 集合框架概述
1.1.1 容器簡介
到目前為止,我們已經學習了如何創建多個不同的對象,定義了這些對象以后,我們就可以利用它們來做一些有意義的事情。
舉例來說,假設要存儲許多雇員,不同的雇員的區別僅在于雇員的身份證號。我們可以通過身份證號來順序存儲每個雇員,但是在內存中實現呢?是不是要準備足夠的內存來存儲 1000個雇員,然后再將這些雇員逐一插入?如果已經插入了 500條記錄,這時需要插入一 個身份證號較低的新雇員,該怎么辦呢?是在內存中將 500 條記錄全部下移后,再從開頭插 入新的記錄? 還是創建一個映射來記住每個對象的位置?當決定如何存儲對象的集合時,必須考慮如下問題。
對于對象集合,必須執行的操作主要以下三種:
?添加新的對象
?刪除對象
?查找對象
我們必須確定如何將新的對象添加到集合中??梢詫ο筇砑拥郊系哪┪?、開頭或者中間的某個邏輯位置。
從集合中刪除一個對象后,對象集合中現有對象會有什么影響呢?可能必須將內存移來 移去,或者就在現有對象所駐留的內存位置下一個“洞”?
在內存中建立對象集合后,必須確定如何定位特定對象??山⒁环N機制,利用該機制 可根據某些搜索條件(例如身份證號)直接定位到目標對象;否則,便需要遍歷集合中的每 個對象,直到找到要查找的對象為止。
前面大家已經學習過了數組。數組的作用是可以存取一組數據。但是它卻存在一些缺點, 使得無法使用它來比較方便快捷的完成上述應用場景的要求。
首先,在很多數情況下面,我們需要能夠存儲一組數據的容器,這一點雖然數組可 以實現,但是如果我們需要存儲的數據的個數多少并不確定。比如說,我們需要在 容器里面存儲某個應用系統的當前的所有的在線用戶信息,而當前的在線用戶信息是時刻都可能在變化的。 也就是說,我們需要一種存儲數據的容器,它能夠自動的改變這個容器的所能存放的數據數量的大小。這一點上,如果使用數組來存儲的話,就顯得十分的笨拙。
我們再假設這樣一種場景:假定一個購物網站,經過一段時間的運行,我們已經存儲了一系列的購物清單了,購物清單中有商品信息。如果我們想要知道這段時間里 面有多少種商品被銷售出去了。那么我們就需要一個容器能夠自動的過濾掉購物清 單中的關于商品的重復信息。如果使用數組,這也是很難實現的。
最后再想想,我們經常會遇到這種情況,我知道某個人的帳號名稱,希望能夠進一 步了解這個人的其他的一些信息。也就是說,我們在一個地方存放一些用戶信息, 我們希望能夠通過用戶的帳號來查找到對應的該用戶的其他的一些信息。再舉個查字典例子:假設我們希望使用一個容器來存放單詞以及對于這個單詞的解釋,而當 我們想要查找某個單詞的意思的時候,能夠根據提供的單詞在這個容器中找到對應的單詞的解釋。如果使用數組來實現的話,就更加的困難了。
為解決這些問題,Java里面就設計了容器集合,不同的容器集合以不同的格式保存對象。
數學背景
在常見用法中,集合(collection)和數學上直觀的集(set)的概念是相同的。集是 一個唯一項組,也就是說組中沒有重復項。實際上, “集合框架”包含了一個 Set 接口和許 多具體的 Set 類。但正式的集概念卻比 Java 技術提前了一個世紀,那時英國數學家 George Boole 按邏輯正式的定義了集的概念。大部分人在小學時通過我們熟悉的維恩圖 引入的“集的交”和“集的并”學到過一些集的理論。
集的基本屬性如下:
?集內只包含每項的一個實例
?集可以是有限的,也可以是無限的
?可以定義抽象概念
集不僅是邏輯學、數學和計算機科學的基礎,對于商業和系統的日常應用來說,它也很實用。
“連接池”這一概念就是數據庫服務器的一個開放連接集。 Web 服務器必須管理客戶機和連 接集。文件描述符提供了操作系統中另一個集的示例。
映射是一種特別的集。它是一種對(pair)集,每個對表示一個元素到另一元素的單向映射。 一些映射示例有:
? IP地址到域名(DNS)的映射
? 關鍵字到數據庫記錄的映射
? 字典(詞到含義的映射)
? 2 進制到 10 進制轉換的映射
就像集一樣,映射背后的思想比 Java 編程語言早的多,甚至比計算機科學還早。而 Java 中的 Map 就是映射的一種表現形式。
1.1.1 容器的分類
既然您已經具備了一些集的理論,您應該能夠更輕松的理解“集合框架”。 “集合框架” 由一組用來操作對象的接口組成。不同接口描述不同類型的組。在很大程度上,一旦您理解 了接口,您就理解了框架。雖然您總要創建接口特定的實現,但訪問實際集合的方法應該限制在接口方法的使用上;因此,允許您更改基本的數據結構而不必改變其它代碼??蚣芙涌?層次結構如下圖所示。
Java 容器類類庫的用途是“保存對象”,并將其劃分為兩個不同的概念:
1) Collection 。 一組對立的元素,通常這些元素都服從某種規則。List必須保持元素特定 的順序,而 Set 不能有重復元素。
2) Map 。 一組 成對的“鍵值對”對象。初看起來這似乎應該是一個 Collection ,其元素 是成對的對象,但是這樣的設計實現起來太笨拙了,于是我們將Map明確的提取出來形 成一個獨立的概念。另一方面,如果使用Collection表示Map的部分內容,會便于查看 此部分內容。因此Map 一樣容易擴展成多維Map,無需增加新的概念,只要讓Map 中的鍵值對的每個"值”也是一個Map即可。
Collection 和 Map 的區別在于容器中每個位置保存的元素個數。 Collection 每個位置只能保 存一個元素(對象)。此類容器包括:List,它以特定的順序保存一組元素;Set則是元素 不能重復。
Map保存的是“鍵值對”,就像一個小型數據庫。我們可以通過“鍵”找到該鍵對應的“值”。
? Collection -對象之間沒有指定的順序,允許重復元素。
? Set - 對象之間沒有指定的順序,不允許重復元素
? List- 對象之間有指定的順序,允許重復元素,并引入位置下標。
? Map - 接口用于保存關鍵字(Key)和數值(Value)的集合,集合中的每個對象 加入時都提供數值和關鍵字。 Map 接口既不繼承 Set 也不繼承 Collection。
List、Set、Map共同的實現基礎是Object數組。
除了四個歷史集合類外, Java2 框架還引入了六個集合實現,如下表所示。
這里沒有 Collection 接口的實現,接下來我們再來看一下下面的這張關于集合框架的大 圖:
這張圖看起來有點嚇人,熟悉之后就會發現其實只有三種容器:Map, List和Set,它 們各自有兩個三個實現版本。
1.2 Collection
1.2.1 常用方法
Collection 接口用于表示任何對象或元素組。想要盡可能以常規方式處理一組元素時, 就使用這一接口。Collection在前面的大圖也可以看出,它是List和Set的父類。并且它本 身也是一個接口。它定義了作為集合所應該擁有的一些方法。如下:
注意: 集合必須只有對象,集合中的元素不能是基本數據類型。
Collection 接口支持如添加和除去等基本操作。設法除去一個元素時,如果這個元素存在, 除去的僅僅是集合中此元素的一個實例。
? boolean add(Object element)
? boolean remove(Object element)
Collection 接口還支持查詢操作:
? int size()
? boolean isEmpty()
? boolean contains(Object element)
? Iterator iterator()
組操作 :Collection 接口支持的其它操作,要么是作用于元素組的任務,要么是同時作用 于整個集合的任務。
? boolean containsAll(Collection collection)
? boolean addAll(Collection collection)
? void clear()
? void removeAll(Collection collection)
? void retainAll(Collection collection)
containsAll() 方法允許您查找當前集合是否包含了另一個集合的所有元素,即另一個 集合是否是當前集合的子集。其余方法是可選的,因為特定的集合可能不支持集合更改。 addAll() 方法確保另一個集合中的所有元素都被添加到當前的集合中,通常稱為并。 clear() 方法從當前集合中除去所有元素。 removeAll() 方法類似于 clear() ,但 只除去了元素的一個子集。 retainAll() 方法類似于 removeAll() 方法,不過可能 感到它所做的與前面正好相反:它從當前集合中除去不屬于另一個集合的元素,即交。
我們看一個簡單的例子,來了解一下集合類的基本方法的使用:
import java.util.*; public class CollectionToArray { public static void main(String[] args) { Collection collection1 = new ArrayList();//倉U建一個集合對象 collection1.add("000"); //添加對象到Collection 集合中 collection1.add("111"); collection1.add("222"); System.out.println("集合collection1的大小:"+collection1.size ()); System.out.println("集合collection1的內容: "+collection1); collection1.remove ("000") ;//從集合collection1中移除掉"000"這個 對象 System.out.println("集合collection1移除 000 后的內容:"+collection1); System.out.println("集合collection1中是否包含000 :"+collection1.contains("000")); System.out.println("集合collection中是否包含111:"+collection1.contains("111")); Collection collection2 = new ArrayList(); collection2.addAll(collection1);//將collection1集合中的元素全部添加到collection2 中 System.out.println("集合collection2的內容:"+collection2); collection2.clear();//清空集合 collection1 中的元素 System.out.println("集合collection2是否為空 :"+collection2.isEmpty()); //將集合collection1轉化為數組 Object s[] = collection1.toArray(); for(int i=0;i<s.length;i++){ System.out.println(s[i]); } } }運行結果為:
集合collection1的大小:3
集合 collection1 的內容:[000, 111, 222]
集合collection1移除000后的內容:[111, 222]
集合collection1中是否包含000 : false
集合collection1中是否包含111 : true
集合 collection2 的內容:[111, 222]
集合collection2是否為空:true
111
222
這里需要注意的是, Collection 它僅僅只是一個接口,而我們真正使用的時候,確是創建該接口的一個實現類。做為集合的接口,它定義了所有屬于集合的類所都應該具有的一些方法。
而 ArrayList (列表)類是集合類的一種實現方式。
這里需要一提的是,因為Collection的實現基礎是數組,所以有轉換為Object數組的方法:
? Object[] toArray()
? Object[] toArray(Object[] a)
其中第二個方法Object[] toArray(Object[] a)的參數a 應該是集合中所有存放的對象的類的父類。
1.2.2 迭代器
任何容器類,都必須有某種方式可以將東西放進去,然后由某種方式將東西取出來。畢竟,存放事物是容器最基本的工作。對于ArrayList,add()是插入對象的方法,而get()是 取出元素的方式之一。ArrayList很靈活,可以隨時選取任意的元素,或使用不同的下標一次選取多個元素。
如果從更高層的角度思考,會發現這里有一個缺點:要使用容器,必須知道其中元素的確切 類型。初看起來這沒有什么不好的,但是考慮如下情況:如果原本是ArrayList,但是后來 考慮到容器的特點,你想換用 Set ,應該怎么做?或者你打算寫通用的代碼,它們只是使用 容器,不知道或者說不關心容器的類型,那么如何才能不重寫代碼就可以應用于不同類型的容器?
所以迭代器(Iterator)的概念,也是出于一種設計模式就是為達成此目的而形成的。所以Collection不提供get()方法。如果要遍歷Collectin中的元素就必須用Iterator。
迭代器(Iterator)本身就是一個對象,它的工作就是遍歷并選擇集合序列中的對象,而 客戶端的程序員不必知道或關心該序列底層的結構。此外,迭代器通常被稱為“輕量級”對 象,創建它的代價小。但是,它也有一些限制,例如,某些迭代器只能單向移動。
Collection 接口的 iterator() 方法返回一個 Iterator。 Iterator 和您可 能已經熟悉的 Enumeration 接口類似。使用 Iterator 接口方法,您可以從頭至尾遍 歷集合,并安全的從底層Collection中除去元素。
下面,我們看一個對于迭代器的簡單使用:
import java.util.ArrayList; import java.util.Collection; import java.util.Iterator;public class IteratorDemo { public static void main(String[] args) { Collection collection = new ArrayList(); collection.add("s1"); collection.add("s2"); collection.add("s3"); Iterator iterator = collection.iterator(); // 得到一個迭代器 while (iterator.hasNext()) { Object element = iterator.next(); System.out.println("iterator = " + element); } if (collection.isEmpty()) System.out.println("collection is Empty!"); else System.out.println("collection is not Empty! size="+collection.size()); Iterator iterator2 = collection.iterator(); while (iterator2. hasNext()) { // 移除元素 Object element = iterator2.next(); System.out.println("remove: " + element); iterator2.remove(); } Iterator iterator3 = collection.iterator(); if (!iterator3.hasNext()) {//察看是否還有元素 System.out.println("還有元素"); } if (collection.isEmpty()) System.out.println("collection is Empty!"); //使用 collection.isEmpty()方法來判斷 } }程序的運行結果為:
iterator = s1
iterator = s2
iterator = s3
collection is not Empty! size=3
remove: s1
remove: s2
remove: s3
還有元素
collection is Empty!
可以看到, Java 的 Collection 的 Iterator 能夠用來:
需要注意的是:方法刪除由next方法返回的最后一個元素,在每次調用next時,remove方
法只能被調用一次 。
大家看,Java實現的這個迭代器的使用就是如此的簡單。Iterator (迭代器)雖然功能簡 單,但仍然可以幫助我們解決許多問題,同時針對List還有一個更復雜更高級的ListIterator。 您可以在下面的List講解中得到進一步的了解。
1.3 List
1.3.1 概述
前面我們講述的Collection接口實際上并沒有直接的實現類。而List是容器的一種,表示列表的意思。當我們不知道存儲的數據有多少的情況,我們就可以使用List來完成存儲數據的工作。例如前面提到的一種場景。我們想要在保存一個應用系統當前的在線 用戶的信息。我們就可以使用一個List來存儲。因為List的最大的特點就是能夠自動 的根據插入的數據量來動態改變容器的大小。下面我們先看看List接口的一些常用方法。
1.3.2 常用方法
List 就是列表的意思,它是 Collection 的一種,即繼承了 Collection 接口,以定義 一個允許重復項的有序集合。該接口不但能夠對列表的一部分進行處理,還添加了面向位置 的操作。 List 是按對象的進入順序進行保存對象,而不做排序或編輯操作。它除了擁有 Collection 接口的所有的方法外還擁有一些其他的方法。
面向位置的操作包括插入某個元素或 Collection 的功能,還包括獲取、除去或更改元素的功能。在 List 中搜索元素可以從列表的頭部或尾部開始,如果找到元素,還將報 告元素所在的位置。
? void add(int index, Object element) :添加對象 element 到位置 index 上
? boolean addAll(int index, Collection collection) :在 index 位置后添加容器 collection 中所有的元素
? Object get(int index) :取出下標為 index 的位置的元素
? int indexOf(Object element) :查找對象 element 在 List 中第一次出現的位置
? int lastIndexOf(Object element) :查找對象 element 在 List 中最后出現的位置
? Object remove(int index) :刪除 index 位置上的元素
? Object set(int index, Object element) :將 index 位置上的對象替換為 element 并返回 老的元素。
先看一下下面表格:
在“集合框架”中有兩種常規的 List 實現: ArrayList 和 LinkedList 。使用兩種List 實現的哪一種取決于您特定的需要。如果要支持隨機訪問,而不必在除尾部的任何位 置插入或除去元素,那么, ArrayList 提供了可選的集合。但如果,您要頻繁的從列表的 中間位置添加和除去元素,而只要順序的訪問列表元素,那么, LinkedList 實現更好。
我們以ArrayList為例,先看一個簡單的例子:
例子中,我們把12個月份存放到ArrayList中,然后用一個循環,并使用get ()方法將列表中的對象都取出來。
而 LinkedList 添加了一些處理列表兩端元素的方法(下圖只顯示了新方法):
使用這些新方法,您就可以輕松的把 LinkedList 當作一個堆棧、隊列或其它面向端點的數據結構。
我們再來看另外一個使用LinkedList來實現一個簡單的隊列的例子:
運行程序產生了以下輸出。請注意,與 Set 不同的是 List 允許重復。
[Clara, Elizabeth, Gene, Elizabeth, Bernadine]
[Clara, Elizabeth, Gene]
該程序演示了具體 List 類的使用。第一部分,創建一個由 ArrayList 支持的 List。 填充完列表以后,特定條目就得到了。示例的 LinkedList 部分把 LinkedList 當作一 個隊列,從隊列頭部添加東西,從尾部除去。
List 接口不但以位置友好的方式遍歷整個列表,還能處理集合的子集:
? ListIterator listIterator():返回一個 ListIterator 迭代器,默認開始位置為 0
? ListIterator listIterator(int startIndex):返 回一個 ListIterator 迭代器,開始位置為 startIndex
? List subList(int fromIndex, int toIndex) : 返回一個子列表 List ,元素存放為從 fromIndex 到 toIndex 之前的一個元素。
處理 subList() 時,位于 fromIndex 的元素在子列表中,而位于 toIndex 的元素則 不是,提醒這一點很重要。以下 for-loop 測試案例大致反映了這一點:
for (int i = fromIndex; i < toIndex; i++) { // process element at position i }此外,我們還應該提醒的是:對子列表的更改(如 add()、remove() 和 set() 調用) 對底層 List 也有影響。
ListIterator 接口
ListIterator 接口繼承 Iterator 接口以支持添加或更改底層集合中的元素,還支持雙向訪問。
以下源代碼演示了列表中的反向循環。請注意 ListIterator 最初位于列表尾之后
(list.size()),因為第一個元素的下標是0。
正常情況下,不用 ListIterator 改變某次遍歷集合元素的方向 — 向前或者向后。雖然在技術上可能實現時,但在 previous() 后立刻調用 next() ,返回的是同一個元素。 把調用 next() 和 previous() 的順序顛倒一下,結果相同。
我們看一個 List 的例子:
import j ava.util.*; public class ListIteratorTest { public static void main(String[] args) { List list = new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("ccc"); list.add("ddd"); System.out.println ("下標0開始: " + list.listIterator(0).next()); // next() System.out.println ("下標1開始:" + list.listIterator(1).next()); System.out.println ("子List 1 -3 : " + list.subList(1,3)) ; // 子列表 Listiterator it = list.listIterator() ;//默認從下標0開始 //隱式光標屬性add操作,插入到當前的下標的前面 it.add("sss"); while (it.hasNext()) { System.out.println("next Index = " + it.nextIndex() + ", Object = " + it.next()); } // set屬性 ListIterator it1 = list.listIterator(); it1.next(); it1.set("ooo"); ListIterator it2 = list.listIterator(list.size());// 下標 while (it2 .hasPrevious()) { System.out.println("previous Index = " + it2.previousIndex() + ", Object = " + it2.previous()); } } }程序的執行結果為:
下標0開始:aaa
下標1開始:bbb
子List 1-3:[bbb, ccc]
next Index = 1, Object = aaa
next Index = 2, Object = bbb
next Index = 3, Object = ccc
next Index = 4, Object = ddd
previous Index = 4, Object = ddd
previous Index = 3,Object = ccc
previous Index = 2, Object = bbb
previous Index = 1, Object = aaa
previous Index = 0, Object = ooo
我們還需要稍微再解釋一下 add() 操作。添加一個元素會導致新元素立刻 被添加到隱式光標的前面。因此,添加元素后調用 previous() 會返回新元素, 而調用 next() 則不起作用,返回添加操作之前的下一個元素。下標的顯示方式,如下圖所示:
對于List的基本用法我們學會了,下面我們來進一步了解一下List的實現原理,以便加深我們對于集合的理解。
1.3.3 實現原理
前面己經提了一下Collection的實現基礎都是基于數組的。下面我們就已ArrayList為例,簡單分析一下 ArrayList 列表的實現方式。首先,先看下它的構造函數。
下列表格是在SUN提供的API中的描述:
其中第一個構造函數ArrayList ()和第二構造函數ArrayList (Collection c)是按照 Collection 接口文檔所述,所應該提供兩個構造函數,一個無參數,一個接受另一個 Collection。
第 3 個構造函數:
ArrayList(int initialCapacity) 是 ArrayList 實現的比較重要的構造函數,雖然, 我們不常用它,但是默認的構造函數正是調用帶參數: initialCapacity 的構造函數來實現的。其中參數:initialCapacity表示我們構造的這個ArrayList列表的初始化容量是多大。如果調用默認的構造函數,則表示默認調用該參數為initialCapacity =10 的方式,來進行構建一個 ArrayList 列表對象。
為了更好的理解這個initialCapacity參數的概念,我們先看看ArrayList在Sun提 供的源碼中的實現方式。先看一下它的屬性有哪些:
public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; /** * The array buffer into which the elements of the ArrayList are stored? * The capac ity of the ArrayL ist is the length of this array buffer. * 列表的實現核心屬性:數組。我們使用診數組未進行存放集合中的數據。 * 而我們的初始化參數就是該數組構建時候的長度,即訪數組的性就是initialCapacity */ private transient Object elementData[]; /** * The size of the ArrayList (the number of elements it contains). * 列表中真實數據的存放個數 * @serial */ private int size;ArrayList 繼承了AbstractList 我們主要看看 ArrayList 中的屬性就可以了。
ArrayList中主要包含 2 個屬性:
? private transient Object elementData[];
? private int size;
其中數組:: elementData[] 是列表的實現核心屬性:數組。 我們使用該數組來進行存 放集合中的數據。而我們的初始化參數就是該數組構建時候的長度,即該數組的 length 屬性就是 initialCapacity 參數。
Keys: transient表示被修飾的屬性不是對象持久狀態的一部分,不會自動的序列化。
第2個屬性:size表示列表中真實數據的存放個數。
我們再來看一下ArrayList的構造函數,加深一下ArrayList是基于數組的理解。
public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IIlegalArgumentException("IIlegal Capacity:" + initialCapacity); // 構建一個初始化長度為initialcapacity的數組對象 this.elementData = new Object[initialcapacity]; /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { this(10); }從源碼中可以看到默認的構造函數調用的就是帶參數的構造函數:
public ArrayList(int initialCapacity)
不過參數 initialCapacity = 10。
我們主要看 ArrayList(int initialCapacity) 這個構造函數??梢钥吹?#xff1a; this.elementData = new Object[initialCapacity];
我們就是使用的 initialCapacity 這個參數來創建一個 Object 數組。而我們所有的往該集合對象中存放的數據,就是存放到了這個Object數組中去了。
我們在看看另外一個構造函數的源碼:
* 通過另外一個容器對象要構建一個List,構建的數組初始化長度為另外一個容器的size屬性的1.1倍 * @param c the collection whose elements are to be placed into this list. * @throws NullPointerException if the specified collection is null. */ public ArrayList(Collection c) { size = c.size(); // 當前元素的個數為另外一個容器中的元素的個數 // Allow 10% room for growth (擴充1.1倍的容量) elementData = new Object[(int) Math.min((size * 110L) / 100, Integer.MAX_VALUE)]; c.toArray(elementData); } /** * Returns the number of elements in this list. * 返回List中元素的個數 * @returnthe number of elements in this list. */ public int size () { return size; }這里,我們先看size()方法的實現形式。它的作用即是返回size屬性值的大小。 然后我們再看另外一個構造函數 public ArrayList(Collection c) ,該構造函數 的作用是把另外一個容器對象中的元素存放到當前的List對象中。
可以看到,首先,我們是通過調用另外一個容器對象C的方法size()來設置當前的 List 對象的 size 屬性的長度大小。
接下來,就是對elementData數組進行初始化,初始化的大小為原先容器大小的 1.1倍。最后,就是通過使用容器接口中的 Object[] toArray(Object[] a) 方法來把當前容器 中的對象都存放到新的數組elementData中。這樣就完成了一個ArrayList的建立。
可能大家會存在一個問題,那就是,我們建立的這個ArrayList是使用數組來實現 的,但是數組的長度一旦被定下來,就不能改變了。而我們在給ArrayList對象中添加元素的時候,卻沒有長度限制。這個時候, ArrayList 中的 elementData 屬性就必須 存在一個需要動態的擴充容量的機制。我們看下面的代碼,它描述了這個擴充機制:
* 該方法用來判斷當前的數組是否需要擴容,應該擴容多少 * @param minCapacity the desired minimum capacity */public void ensureCapacity(int minCapacity) { modCount++; int oldCapacity = elementData.length; if (minCapacity > oldCapacity) { // 如果minCapacity大于老的elementData數組的長度,那么就需要擴容 Object oldData[] = elementData; // 新的數組的長度為原來長度1.5倍加1,或者為minCapacity int newCapacity = (oldCapacity * 3) / 2 + 1; if (newCapacity < minCapacity) newCapacity = minCapacity; elementData = new Object[newCapacity]; System.arraycopy(oldData, 0, elementData, 0, size); } }這個方法的作用就是用來判斷當前的數組是否需要擴容,應該擴容多少。其中屬性: modCount是繼承自父類,它表示當前的對象對elementData數組進行了多少次擴容, 清空,移除等操作。該屬性相當于是一個對于當前List對象的一個操作記錄日志號。我們主要看下面的代碼實現:
- 如果minCapacity大于oldCapacity,那么我們就對當前的List對象進 行擴容。擴容的的策略為:取 (oldCapacity * 3)/2 + 1和minCapacity 之間更大的那個。然后使用數組拷貝的方法,把以前存放的數據轉移到新的數 組對象中
- 如果 minCapacity 不大于 oldCapacity 那么就不進行擴容
下面我們看看ensureCapacity方法是如何使用的:
* Appends the specified element to the end of this list. * @param o element to be appended to this list. * @return <tt>true</tt> (如果添加成功就返回true). */ public boolean add(Object o) { / /調用ensureCapacity方法來確定是否需要擴容 ensureCapacity (size + 1) ; // Increments modCount!! elementData[size++] = o; return true; }* 該方法用來將另外一個容器C中的元素都添加到當前的List中 * @param c the elements to be inserted into this list. * @return <tt>true</tt> 如果添加的元素個數不為0,就返回true * @throws NullPointerException if the specified collection is null */ public boolean addAll(Collection c) { Object[] a = c.toArray(); int numNew = a.length; // 判斷是否需要擴容 ensureCapacity (size + numNew) ; // Increments modCount!! System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; }上的兩個a d d方法都是往List中添加元素。每次在添加元素的時候,我們就需要判斷 一下,是否需要對于當前的數組進行擴容。
我們主要看看 public boolean add (Object o)方法,可以發現在添加一個元素到容 器中的時候,首先我們會判斷是否需要擴容。因為只增加一個元素,所以擴容的大小判斷也 就為當前的size + 1來進行判斷。然后,就把新添加的元素放到數組elementData中。
第二個方法 public boolean addAll(Collection c) 也是同樣的原理。將新的元素放到 elementData 數組之后。同時改變當前 List 對象的 size 屬性。
類似的 List 中的其他的方法也都是基于數組進行操作的。大家有興趣可以看看源碼中的更多的實現方式。
最后我們再看看如何判斷在集合中是否已經存在某一個對象的:
由源碼中我們可以看到, public boolean contains(Object elem) 方法是通過調用 public int indexOf(Object elem) 方法來判斷是否在集合中存在某個對象elem。我們看看indexOf方法的具體實現。
? 首先我們判斷一下elem對象是否為null ,如果為null的話,那么遍歷數組 elementData 把第一個出現 null的位置返回。
? 如果 elem 不為 null 的話,我們也是遍歷數組 elementData ,并通過調用 elem對象的equals()方法來得到第一個相等的元素的位置。
這里我們可以發現,ArrayList中用來判斷是否包含一個對象,調用的是各個對象自己實現的 equals() 方法。在前面的高級特性里面,我們可以知道:如果要判斷一個類的一 個實例對象是否等于另外一個對象, 那么我們就需要自己覆寫 Object 類的 public boolean equals(Object obj) 方法。如果不覆寫該方法的話,那么就會調用 Object 的 equals() 方法來進行判斷。這就相當于比較兩個對象的內存應用地址是否相等了。
在集合框架中,不僅僅是List,所有的集合類,如果需要判斷里面是否存放了的某個對象,都是調用該對象的equals()方法來進行處理的。
1.4 Map
1.4.1 概述
數學中的映射關系在Java中就是通過Map來實現的。它表示,里面存儲的元素是一個對 (pair) ,我們通過一個對象,可以在這個映射關系中找到另外一個和這個對象相關的東西。 前面提到的我們對于根據帳號名得到對應的人員的信息,就屬于這種情況的應用。我們講一 個人員的帳戶名和這人員的信息作了一個映射關系,也就是說,我們把帳戶名和人員信息當 成了一個“鍵值對”,“鍵”就是帳戶名,“值”就是人員信息。下面我們先看看Map接口的常用方法。
1.4.2 常用方法
Map 接口不是 Collection 接口的繼承。而是從自己的用于維護鍵-值關聯的接口層次結
構入手。按定義,該接口描述了從不重復的鍵到值的映射。
我們可以把這個接口方法分成三組操作:改變、查詢和提供可選視圖。
改變操作允許您從映射中添加和除去鍵-值對。鍵和值都可以為null。但是,您不能把Map 作為一個鍵或值添加給自身。
? Object put(Object key,Object value):用來存放一個鍵-值對 Map 中
? Object remove(Object key):根據key (鍵),移除一個鍵-值對,并將值返回
? void putAll(Map mapping) :將另外一個 Map 中的元素存入當前的 Map 中
? void clear():清空當前 Map 中的元素
查詢操作允許您檢查映射內容:
? Object get (Object key):根據key (鍵)取得對應的值
? boolean containsKey (Object key):判斷 Map 中是否存在某鍵(key)
? boolean containsValue(Object value):判斷 Map 中是否存在某值 (value)
? int size ():返回Map中鍵-值對的個數
? boolean isEmpty():判斷當前 Map 是否為空
最后一組方法允許您把鍵或值的組作為集合來處理:
? public Set keySet ():返回所有的鍵(key),并使用Set容器存放
? public Collection values ():返回所有的值(Value),并使用 Collection 存放
? public Set entrySet() :返回一個實現 Map.Entry 接口的元素 Set, 因為映射中鍵的集合必須是唯一的,就使用 Set 來支持。因為映射中值的集合可能不唯一, 就使用 Collection 來支持。最后一個方法返回一個實現 Map.Entry 接口的元素 Set。
我們看看 Map 的常用實現類的比較,如下表:
下面我們看一個簡單的例子:
import java.util.*; public class MapTest { public static void main(String[] args) { Map map1 = new HashMap(); Map map2 = new HashMap(); map1.put("1","aaa1"); map1.put("2","bbb2"); map2.put("10","aaaa10"); map2.put("11","bbbb11"); // 根據鍵"1"取得值:"aaa1" System.out.println("map1.get(\"1\")="+map1.get("1")); //根據鍵"1"移除鍵值對"1"-"aaa1" System.out.println("map1.remove (\"1\") =" + map1.remove ("1")); System.out.println("map1.get(\"1\")=" + map1.get("1")); map1.putAll(map2); //將map2全部元素放入map1中 map2.clear(); // 清空map2 System.out.println("map1 IsEmpty?=" + map1.isEmpty()); System.out.println("map2 IsEmpty?=" + map2.isEmpty()); System.out.println("map1中的鍵值對的個數size = " + map1.size()); System.out.println("KeySet=" + map1.keySet()); // set System.out.println("values=" + map1.values()); // Collection System.out.println("entrySet=" + map1.entrySet()); System.out.println("map1是否包含鍵:11 = " + map1.containsKey("11")); System.out.println ( "map1是否包含值:aaa1 = " + map1.containsValue("aaa1")); } } 運行輸出結果為: map1.get("1")=aaa1 map1.remove("1")=aaa1 map1.get("1")=null map1 IsEmpty?=false map2 IsEmpty?=true map1中的鍵值對的個數size = 3 KeySet=[10, 2, 11] values=[aaaa10, bbb2, bbbb11] entrySet=[10=aaaa10, 2=bbb2, 11=bbbb11] map1 是否包含鍵:11 = true map1 是否包含值: aaa1 = false在該例子中,我們創建一個HashMap,并使用了一下Map接口中的各個方法。
其中 Map 中的 entrySet() 方法先提一下,該方法返回一個實現 Map.Entry 接口的對象集合。集合中每個對象都是底層 Map 中一個特定的鍵-值對。
Map.Entry接口是Map接口中的一個內部接口,該內部接口的實現類存放的是鍵值對。 在下面的實現原理中,我們會對這方面再作介紹,現在我們先不管它的具體實現。 我們再看看排序的 Map 是如何使用:
import java.util.*; public class MapSortExample { public static void main(String args[]) { Map map1 = new HashMap(); Map map2 = new LinkedHashMap(); for (int i = 0; i < 10; i++) { double s = Math.random() * 100; // 產生一個隨機數,并將其放入Map中 map1.put(new Integer((int)s), "第" + i + "個放入的元素:" + s + "\n"); map2.put(new Integer((int)s), "第" + i + "個放入的元素:" + s + "\n"); } System.out.println("未排序前HashMap:" + map1); System.out.println("未排序前LinkedHashMap: " + map2); // 使用TreeMap來對另外的Map進行重構和排序 Map sortedMap = new TreeMap(map1); System.out.println("排序后:" + sortedMap); System.out.println("排序后:" + new TreeMap (map2)); } } 該程序的一次運行結果為: 未排序前HashMap:{ 64=第 1個放入的元素:64.05341725531845, 15=第 9 個放入的元素:15.249165766266382, 2=第 4 個放入的元素:2.66794706854534, 77=第 0 個放入的元素:77.28814965781416, 97=第 5 個放入的元素:97.32893518378948, 99=第 2 個放入的元素:99.99412014935982, 60=第 8 個放入的元素:60.91451419025399, 6=第 3 個放入的元素:6.286974058646977, 1=第 7 個放入的元素:1.8261658496439903, 48=第 6 個放入的元素:48.736039522423106 } 未排序前LinkedHashMap: { 77=第 0 個放入的元素:77.28814965781416, 64=第 1 個放入的元素:64.05341725531845, 99=第 2 個放入的元素:99.99412014935982, 6=第 3 個放入的元素:6.286974058646977, 2=第 4 個放入的元素:2.66794706854534, 97=第 5 個放入的元素:97.32893518378948, 48=第 6 個放入的元素:48.736039522423106, 1=第 7 個放入的元素:1.8261658496439903, 60=第 8 個放入的元素:60.91451419025399, 15=第 9 個放入的元素:15.249165766266382 } 排序后:{ 1=第7個放入的元素:1.8261658496439903, 2=第 4 個放入的元素:2.66794706854534, 6=第 3 個放入的元素:6.286974058646977, 15=第 9 個放入的元素:15.249165766266382, 48=第 6 個放入的元素:48.736039522423106, 60=第 8 個放入的元素:60.91451419025399, 64=第 1 個放入的元素:64.05341725531845, 77=第 0 個放入的元素:77.28814965781416, 97=第 5 個放入的元素:97.32893518378948, 99=第 2 個放入的元素:99.99412014935982 } 排序后:{ 1=第7個放入的元素:1.8261658496439903, 2=第 4 個放入的元素:2.66794706854534, 6=第 3 個放入的元素:6.286974058646977, 15=第 9 個放入的元素:15.249165766266382, 48=第 6 個放入的元素:48.736039522423106, 60=第 8 個放入的元素:60.91451419025399, 64=第 1 個放入的元素:64.05341725531845, 77=第 0 個放入的元素:77.28814965781416, 97=第 5 個放入的元素:97.32893518378948, 99=第 2 個放入的元素:99.99412014935982 }從運行結果,我們可以看出,HashMap的存入順序和輸出順序無關。而LinkedHashMap 則保留了鍵值對的存入順序。TreeMap則是對Map中的元素進行排序。在實際的使用中我 們也經常這樣做:使用HashMap或者LinkedHashMap來存放元素,當所有的元素都存放完成后,如果使用是需要一個經過排序的 Map 的話,我們再使用 TreeMap 來重構原來的 Map對象。這樣做的好處是:因為HashMap和LinkedHashMap存儲數據的速度比直接使 用TreeMap 要快,存取效率要高。當完成了所有的元素的存放后,我們再對整個的 Map中的元素進行排序。這樣可以提高整個程序的運行的效率,縮短執行時間。
這里需要注意的是,TreeMap中是根據鍵(Key)進行排序的。而如果我們要使用TreeMap 來進行正常的排序的話,Key中存放的對象必須實現Comparable接口。
1.4.3 Comparable 接口
在 java.lang 包中, Comparable 接口適用于一個類有自然順序的時候。假定對象集合 是同一類型,該接口允許您把集合排序成自然順序。
它只有一個方法: compareTo() 方法,用來比較當前實例和作為參數傳入的元素。 如果排序過程中當前實例出現在參數前(當前實例比參數大),就返回某個負值。如果當前 實例出現在參數后(當前實例比參數小),則返回正值。否則,返回零。如果這里不要求零返回值表示元素相等。零返回值可以只是表示兩個對象在排序的時候排在同一個位置。
上面例子中的整形的包裝類: Integer 就實現了該接口。我們可以看一下這個類的源碼:
public final class Integer extends Number implements Comparable { public int compareTo(Object o) { return compareTo((Integer)o); }public int compareTo(Integer anotherInteger) { int thisVal = this.value; int anotherVal = anotherInteger.value; return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1)); }可以看到 compareTo 方法里面通過判斷當前的 Integer 對象的值是否大于傳入的參數的值來得到返回值的。
在Java 2 SDK,版本1.2中有十四個類實現Comparable接口。下表展示了它們的自 然排序。雖然一些類共享同一種自然排序,但只有相互可比的類才能排序。
這里只是簡單的介紹一下排序接口,如果要詳細的了解排序部分內容的話,可以參考文章最 后的附錄部分對于排序的更加詳細的描述。
我們再回到Map中來,Java提高的API中除了上面介紹的幾種Map比較常用以為還有一些Map,大家可以了解一下:
? WeakHashMap: WeakHashMap 是 Map 的一個特殊實現,它只用于存儲對鍵的弱引用。當映射的某個鍵在 WeakHashMap 的外部不再被引用時,就允許垃圾收集器收集映射中相應的鍵值對。使用 WeakHashMap 有益于保持類似注冊表的數據結構,其中條目的鍵不再被任何線程訪問時,此條目就沒用了。
? IdentifyHashMap : Map 的一種特性實現, 關鍵屬性的 hash 碼不是由hashCode() 方法計算,而是由 System.identityHashCode 方法計算,使用 == 進行比較而不是 equals() 方法。
通過簡單的對Map中各個常用實現類的使用,為了更好的理解Map,下面我們再來 了解一下 Map 的實現原理。
1.4.4 實現原理
有的人可能會認為Map會繼承Collection。在數學中,映射只是對(pair)的集合。但是,在“集合框架”中,接口 Map 和 Collection 在層次結構沒有任何親緣關系,它們是截然不同的。這種差別的原因與 Set 和 Map 在 Java 庫中使用的方法有關。 Map 的典型應用是訪問按關鍵字存儲的值。它支持一系列集合操作的全部,但操作的是鍵-值對, 而不是單個獨立的元素。因此 Map 需要支持 get() 和 put() 的基本操作,而 Set 不 需要。此外,還有返回 Map 對象的 Set 視圖的方法:
Set set = aMap.keySet();
下面我們以HashMap為例,對Map的實現機制作一下更加深入一點的理解。
因為HashMap里面使用Hash算法,所以在理解HashMap之前,我們需要先了解一下Hash 算法和 Hash 表。
Hash,一般翻譯做“散列”,也有直接音譯為"哈希"的,就是把任意長度的輸入(又叫做 預映射,pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小于輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。
說的通俗一點,Hash算法的意義在于提供了一種快速存取數據的方法,它用一種算法建 立鍵值與真實值之間的對應關系,(每一個真實值只能有一個鍵值,但是一個鍵值可以對應多 個真實值),這樣可以快速在數組等里面存取數據。
看下圖:
我們建立一個HashTable (哈希表),該表的長度為N,然后我們分別在該表中的格子中存放 不同的元素。每個格子下面存放的元素又是以鏈表的方式存放元素。
? 當添加一個新的元素Entry的時候,首先我們通過一個Hash函數計算出這個Entry元 素的Hash值hashcode。通過該hashcode值,就可以直接定位出我們應該把這個Entry 元素存入到Hash表的哪個格子中,如果該格子中己經存在元素了,那么只要把新的 Entry 元存放到這個鏈表中即可。
? 如果要查找一個元素Entry的時候,也同樣的方式,通過Hash函數計算出這個Entry 元素的Hash值hashcode。然后通過該hashcode值,就可以直接找到這個Entry是存放到哪個格子中的。接下來就對該格子存放的鏈表元素進行逐個的比較查找就可以了。
舉一個比較簡單的例子來說明這個算法的運算方式:
假定我們有一個長度為8的Hash表(可以理解為一個長度為8的數組)。在這個Hash表中 存放數字:如下表
假定我們的Hash函數為:
Hashcode = X%8 ,即對 8 取余數。
其中X就是我們需要放入Hash表中的數字,而這個函數返回的Hashcode就是Hash碼。
假定我們有下面10個數字需要依次存入到這個Hash表中:
11, 23, 44, 9, 6, 32, 12, 45, 57, 89
通過上面的Hash函數,我們可以得到分別對應的Hash碼:
11 -- 3; 23 -- 7;44 -- 4;9 -- 1;6 -- 6;32 -- 0;12 -- 4;45 -- 5;57 -- 1;89 -- 1;
計算出來的Hash碼分別代表該數字應該存放到Hash表中的哪個對應數字的格子中。如果改格子中已經有數字存在了,那么就以鏈表的方式將數字依次存放在該格子中,如下表:
Hash 表和 Hash 算法的特點就是它的存取速度比數組差一些,但是比起單純的鏈表,在查找和存儲方面卻要好很多。同時數組也不利于數據的重構和排序等方面的要求。
更具體的說明,讀者可以參考數據結構相關方面的書籍。
簡單的了解了一下Hash算法后,我們就來看看HashMap的屬性有哪些:
public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {/* * 哈希表,Entry對象中存放的是犍值對。并且該數組的長度為2的次方 * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry[] table; /* * 鍵值對的個數 * The number of key-value mappings contained in this identity hash map? */ transient int size; /* * The load factor for the hash table. * 哈希表的負載因子 * @serial * / final float loadFactor;里面最重要的3個屬性:
Entry對象是Map接口中的一個內部接口。即使用它來保存鍵值對。 我們看看這個Entry內部接口在HashMap中的實現:
static class Entry implements Map.Entry { final Object key; // 鍵,并且不可修改 Object value; // 值 final int hash; // hash碼 Entry next; // 當前鍵值對的下一個鍵值對 Entry (int h, Object k, Object v, Entry n) { value = v; next =n; key = k; hash = h; }通過查看源碼,我們可以看到Entry類有點類似一個單向鏈表。其中:
final Object key 和 Object value存放的就是我們放入Map中的鍵值對。 而屬性Entry next表示當前鍵值對的下一個鍵值對。
接下來,我們看看 HashMap 的主要的構造函數:
我們主要看看 public HashMap (int initialcapacity, float loadFactor) 因為,另外兩個構造函數實際也是同樣的方式進行構建一個HashMap的。 該構造函數:
該put方法是用來添加一個鍵值對(key-value)到Map中,如果Map中己經存在相同的 鍵的鍵值對的話,那么就把新的值覆蓋老的值,并把老的值返回給方法的調用者。如果不存在一樣的鍵,那么就返回 null 。我們看看方法的具體實現:
最后我們看看一個鍵值對是如何添加到各個格子中的鏈表中的:
void addEntry(int hash, Object key, Object value, int bucketIndex) { // 創建一個Entry對象用來存放鍵值對 // 并把原來的格子中鏈表的第一位置的元素作為當前Entry對象的下一個元素 // 換句話說,就是把當前創建的Entry對象,加到鏈表的第一個位置,其它的掛到它的后面 table[bucketIndex] = new Entry(hash, k, value, table[bucketIndex]); // 如果存放元素的個數大于重構因子threshold,那么就進行重構 if (size++ >= threshold) {resize(2 * table.length); }我們先看void addEntry(int hash, Obj ect key, Obj ect value, int bucketIndex)方法,該方法的作用就用來添加一個鍵值對到Hash表的第bucketIndex 個格子中的鏈表中去。這個方法的工作就是:
之所以需要重構,也是基于性能考慮。大家可以考慮這樣一種情況,假定我們的Hash 表只有4個格子,那么我們所有的數據都是放到這4個格子中。如果存儲的數據量比較大 的話,例如 100。這個時候,我們就會發現,在這個 Hash 表中的 4 個格子存放的 4 個長長的鏈表。而我們每次查找元素的時候,其實相當于就是遍歷鏈表了。這種情況下,我們用 這個Hash表來存取數據的性能實際上和使用鏈表差不多了。
但是如果我們對這個 Hash 表進行重構,換為使用 Hash 表長度為 200 的表來存儲這 100個數據,那么平均 2個格子里面才會存放一個數據。這個時候我們查找的數據的速度就會非常的快。因為基本上每個格子中存放的鏈表都不會很長,所以我們遍歷鏈表的次數也 就很少,這樣也就加快了查找速度。但是這個時候又存在了另外的一個問題。我們使用了至 少200個數據的空間來存放100個數據,這樣就造成至少100個數據空間的浪費。 在速 度和空間上面,我們需要找到一個適合自己的中間值。在HashMap中我們通過負載因子 (loadFactor)來決定應該什么時候重構我們的Hash 表,以達到比較好的性能狀態。
我們再看看重構Hash表的方法:void resize( int newCapacity)是如何實現的:
它的實現方式比較簡單:
對于HashMap中的實現原理,我們就分析到這里。大家可能會發現,HashCode的計算, 是用來定位我們的鍵值對應該放到 Hash 表中哪個格子中的關鍵屬性。 而這個 HashCode的計算方法是調用的各個對象自己的實現的hashCode()方法。而這個方法是 在Object對象中定義的,所以我們自己定義的類如果要在集合中使用的話,就需要正確 的覆寫 hashCode() 方法。下面就介紹一下應該如何正確覆寫 hashCode() 方法。
1.4.5 覆寫 hashCode()
在明白了 HashMap具有哪些功能,以及實現原理后,了解如何寫一個hashCode () 方法就更有意義了。當然,在HashMap中存取一個鍵值對涉及到的另外一個方法為equals (),因為該方法的覆寫在高級特性已經講解了。這里就不做過多的描述。
設計hashCode ()時最重要的因素就是:無論何時,對同一個對象調用hashCode () 都應該生成同樣的值。如果在將一個對象用 put() 方法添加進 HashMap 時產生一個 hashCode ()值,而用get ()取出時卻產生了另外一個hashCode ()值,那么就無法重新 取得該對象了。所以,如果你的hashCode()方法依賴于對象中易變的數據,那用戶就要 小心了,因為此數據發生變化時, hashCode() 就會產生一個不同的 hash 碼,相當于產生 了一個不同的“鍵”。
此外,也不應該使 hashCode() 依賴于具有唯一性的對象信息,尤其是使用 this 的 值,這只能產生很糟糕的hashCode()。因為這樣做無法生成一個新的“鍵",使之與put() 種原始的"鍵值對”中的“鍵”相同。例如,如果我們不覆寫Object的hashCode()方 法,那么調用該方法的時候,就會調用Object的hashCode ()方法的默認實現。Object 的hashCode ()方法,返回的是當前對象的內存地址。下次如果我們需要取一個一樣的“鍵” 對應的鍵值對的時候,我們就無法得到一樣的 hashCode 值了。因為我們后來創建的“鍵” 對象己經不是存入HashMap中的那個內存地址的對象了。
我們看一個簡單的例子,就能更加清楚的理解上面的意思。假定我們寫了一個類: Person (人),我們判斷一個對象“人”是否指向同一個人,只要知道這個人的身份證 號一直就可以了。
先看我們沒有實現 hashCode 的情況:
上面的例子的演示的是,我們在一個HashMap中存放了一些人員的信息。并以這些人 員的身份證最為人員的“鍵”。當有的人員的姓名修改了的情況下,我們需要更新這個 HashMap。同時假如我們知道某個身份證號,想了解這個身份證號對應的人員信息如何, 我們也可以根據這個身份證號在HashMap中得到對應的信息。
而例子的輸出結果表示,我們所做的更新和查找操作都失敗了。失敗的原因就是我們的 身份證類: Code 沒有覆寫 hashCode() 方法。這個時候,當查找一樣的身份證號碼的鍵 值對的時候,使用的是默認的對象的內存地址來進行定位。這樣,后面的所有的身份證號對 象new Code (123)產生的hashCode ()值都是不一樣的。所以導致操作失敗。
下面,我們給Code類加上hashCode ()方法,然后再運行一下程序看看:
再次執行上面的HashCodeEx的結果就為:
HashMap中存放的人員信息: {身份證:456=姓名:李四 身份證:456 ,身份證:789=姓名:王二 身份證:789 ,身份證:123=姓名:張三 身份證:123 } 張三改名后HashMap中存放的人員信息: {身份證:456=姓名:李四 身份證:456 ,身份證:789=姓名:王二 身份證:789 ,身份證:123=姓名:張山 身份證:123 } 查找身份證為:123的人員信息:姓名:張山身份證:123這個時候,我們發現。我們想要做的更新和查找操作都成功了。
對于Map部分的使用和實現,主要就是需要注意存放“鍵值對”中的對象的equals () 方法和 hashCode() 方法的覆寫。如果需要使用到排序的話,那么還需要實現 Comparable 接口中的compareTo ()方法。我們需要注意Map中的“鍵”是不能重復的,而是否重復 的判斷,是通過調用“鍵”對象的equals ()方法來決定的。而在HashMap中查找和存 取"鍵值對”是同時使用hashCode ()方法和equals ()方法來決定的。
1.5 Set
1.5.1 概述
Java中的Set和正好和數學上直觀的築(set)的概念是相同的。Set最大的特性就是 不允許在其中存放的元素是重復的。根據這個特點,我們就可以使用Set這個接口來實現 前面提到的關于商品種類的存儲需求。 Set 可以被用來過濾在其他集合中存放的元素,從 而得到一個沒有包含重復新的集合。
1.5.2 常用方法
按照定義, Set 接口繼承 Collection 接口,而且它不允許集合中存在重復項。所 有原始方法都是現成的,沒有引入新方法。具體的 Set 實現類依賴添加的對象的 equals() 方法來檢查等同性。
我們簡單的描述一下各個方法的作用:
? public int size():返回set中元素的數目,如果 set 包含的元素數大于 Integer.MAX_VALUE,返回 Integer.MAX_VALUE
? public boolean isEmpty() :如果 set 中不含元素,返回 true
? public boolean contains(Object o) :如果 set 包含指定元素,返回 true
? public Iterator iterator():返回 set 中元素的迭代器,元素返回沒有特定的順序
? public Object[] toArray() :返回包含 set 中所有元素的數組
? public Object[] toArray(Object[] a):返回包含set中所有元素的數組,返回數組的運 行時類型是指定數組的運行時類型
? public boolean add(Object o):如果set中不存在指定元素,則向set加入
? public booleanremove(Objecto):如果set中存在指定元素,則從set中刪除
? public boolean removeAll(Collection c):如果 set 包含指定集合,則從 set 中刪除指 定集合的所有元素
? public boolean containsAll(Collection c):如果 set 包含指定集合的所有元素,返回 true。如果指定集合也是一個set,只有是當前set的子集時,方法返回true
? public boolean addAll(Collection c):如果 set 中中不存在指定集合的元素,則向 set 中加入所有元素
? public boolean retainAll(Collection c):只保留 set 中所含的指定集合的元素(可選操 作)。換言之,從set中刪除所有指定集合不包含的元素。如果指定集合也是一個 set,那么該操作修改set的效果是使它的值為兩個set的交集
? public boolean removeAll(Collection c):如果 set 包含指定集合,則從 set 中刪除指 定集合的所有元素
? public void clear():從 set 中刪除所有元素
“集合框架” 支持 Set 接口兩種普通的實現:
HashSet 和 TreeSet 以及 LinkedHashSet。 下表中是Set的常用實現類的描述:
在更多情況下,您會使用 HashSet 存儲重復自由的集合。同時 HashSet 中也是采用 了 Hash 算法的方式進行存取對象元素的。所以添加到 HashSet 的對象對應的類也需要 采用恰當方式來實現 hashCode() 方法。雖然大多數系統類覆蓋了 Object 中缺省的 hashCode() 實現, 但創建您自己的要添加到 HashSet 的類時, 別忘了覆蓋 hashCode() 。
對于Set的使用,我們先以一個簡單的例子來說明:
import java.util.*; public class HashSetDemo { public static void main(String[] args) { Set set1 = new HashSet(); if (set1.add("a")) {//添加成功 System.out.println("1 add true"); } if (set1.add("a")) {//添加失敗 System.out.println("2 add true"); } set1.add("000") ; //添加對象到Set集合中 set1.add("111"); set1.add("222"); System.out.println("集合set1的大小: " + set1.size()); System.out.println("集合set1 的內容:" + set1); set1.remove("000") ;//從集合set1中移除掉 "000" 這個對象 System.out.println("集合set1 移除 000 后的內容:" + set1); System.out.println("集合set1中是否包含000 :" + set1.contains("000")); System.out.println ("集合set1中是否包含 111 :" + set1.contains("111")); Set set2 = new HashSet(); set2.add("111"); set2.addAll(set1); // 將set1集合中的元素全部都加到set2中 System.out.println("集合set2的內容:"+set2 ); set2.clear(); // 清空集合set1中的元素 System.out.println("集合set2是否為空:" + set2.isEmpty()); Iterator iterator = set1.iterator(); // 得到一個迭代器 while (iterator. hasNext()) { Object element = iterator.next(); System.out.println("iterator = " + element); } // 將集合set1轉化為數組 Obj ect s[]= set].toArray(); for(int i=0;i<s.length;i++){ System.out.println(s[i]); } } }程序執行的結果為:
1 add true 集合set]的大小:4 集合 set]的內容:[222, a, 000, ]]]] 集合set]移除000后的內容:[222, a, ]]]] 集合set]中是否包含000 : false 集合set]中是否包含]]]:true 集合 set2 的內容:[222, a, ]]]] 集合set2是否為空:true iterator = 222 iterator = a iterator =]]] 222 a 111從上面的這個簡單的例子中,我們可以發現,Set中的方法與直接使用Collection中的 方法一樣。唯一需要注意的就是Set中存放的元素不能重復。
我們再看一個例子,來了解一下其它的Set的實現類的特性:
package c08; import java.util.*; public class SetSortExample { public static void main(String args[]) { Set set1 = new HashSet(); Set set2 = new LinkedHashSet(); for (int i = 0; i < 5; i++) { //產生一個隨機數,并將其放入Set中 int s = (int)(Math.random() * 100); set1.add(new Integer(s)); set2.add(new Integer(s)); System.out.println ("第 " + i + " 次隨機數產生為:" + s); } System.out.println("未排序前HashSet : " + set1); System.out.println ("未排序前LinkedHashSet: " + set2); // 使用TreeSet來對另外的Set進行重構和排序 Set sortedSet = new TreeSet(set1); System.out.println("排序后TreeSet : " + sortedSet); } }該程序的一次執行結果為:
第0次隨機數產生為:96 第1次隨機數產生為:64 第2次隨機數產生為:14 第3次隨機數產生為:95 第4次隨機數產生為:57 未排序前 HashSet:[64, 96, 95, 57, 14] 未排序前LinkedHashSet:[96, 64, 14, 95, 57 ] 排序后TreeSet:[14, 57, 64, 95, 96]從這個例子中,我們可以知道HashSet的元素存放順序和我們添加進去時候的順序沒 有任何關系,而 LinkedHashSet 則保持元素的添加順序。 TreeSet 則是對我們的 Set 中的元素進行排序存放。
一般來說,當您要從集合中以有序的方式抽取元素時, TreeSet 實現就會有用處。為 了能順利進行,添加到 TreeSet 的元素必須是可排序的。 而您同樣需要對添加到 TreeSet 中的類對象實現 Comparable 接口的支持。對于 Comparable 接口的實現,在 前一小節的Map中己經簡單的介紹了一下。我們暫且假定一棵樹知道如何保持java.lang 包裝程序器類元素的有序狀態。一般說來,先把元素添加到HashSet,再把集合轉換為 TreeSet 來進行有序遍歷會更快。這點和 HashMap 的使用非常的類似。
其實Set的實現原理是基于Map上面的。通過下面我們對Set的進一步分析大家就能 更加清楚的了解這點了。
1.5.3 實現原理
Java中Set的概念和數學中的集合(set)一致,都表示一個集內可以存放的元素是不能重 復的。
前面我們會發現,Set中很多實現類和Map中的一些實現類的使用上非常的相似。而且 前面再講解Map的時候,我們也提到:Map中的“鍵值對”,其中的“鍵”是不能重復的。 這個和Set中的元素不能重復一致。我們以HashSet為例來分析一下,會發現其實Set利用 的就是Map中“鍵”不能重復的特性來實現的。
先看看HashSet中有哪些屬性:
再結合構造函數來看看:
/* * 構造函數就是對HashMap的構建 * Constructs a new, empty set;the backing <tt>HashMap</tt> * default initial capacity(16) and load factor(0.75). */public HashSet() {map = new HashMap(); }public HashSet(int initialCapacity, float loadFactor) {map = new HashMap(initialCapacity,loadFactor);}public HashSet(int initialCapacity) {map = new HashMap(initialCapacity); }通過這些方法,我們可以發現,其實HashSet的實現,全部的操作都是基于HashMap來進行的。我們看看是如何通過HashMap來保證我們的HashSet的元素不重復性的:
/* * Adds the specified element to this set if it is not already contain the specified element. * */ public boolean add(Object o) {return map.put(o,PRESENT) == null; }看到這個操作我們可以發現HashSet的巧妙實現:就是建立一個“鍵值對”,“鍵”就是 我們要存入的對象,“值”則是一個常量。這樣可以確保,我們所需要的存儲的信息之是“鍵”。 而“鍵”在Map中是不能重復的,這就保證了我們存入Set中的所有的元素都不重復。而 判斷是否添加元素成功,則是通過判斷我們向Map中存入的“鍵值對”是否己經存在,如 果存在的話,那么返回值肯定是常量: PRESENT,表示添加失敗。如果不存在,返回值就 為null `表示添加成功。
我們再看看其他的方法實現:
/* * Removes the specified element from this set if it is present * 移出操作即是對Map中的鍵值對進行移出,如果返回值為PRESENT常量,則表示移出成功 * @param o object to be removed from this set, if present. * @return <tt>true</tt> if the set contained the specified element. * */public boolean remove(Object o) {return map.remove(o) == PRESENT; }public int size() {return map.size(); }public boolean contains(Object o) {return map.containsKey(o); }public boolean isEmpty() {return map.isEmpty(); }public Iterator iterator() {return map.keySet().iterator(); }了解了這些后,我們就不難理解,為什么HashMap中需要注意的地方,在HashSet中 也同樣的需要注意。其他的Set的實現類也是差不多的原理。
至此對于Set我們就應該能夠比較好的理解了。
1.6 總結:集合框架中常用類比較
用“集合框架”設計軟件時,記住該框架四個基本接口的下列層次結構關系會有用處:
- Collection接口是一組允許重復的對象。
- Set接口繼承Collection,但不允許重復。
- List接口繼承Collection,允許重復,并引入位置下標。
- Map 接口既不繼承 Set 也不繼承 Collection, 存取的是鍵值對
我們以下面這個圖表來描述一下常用的集合的實現類之間的區別:
2 練習
屬性/方法:
Certificate code: 身份證對象
name: 姓名
cash: 現金
List car: 擁有的汽車,其中存放的是Car對象
boolean buycar(car):買車子
boolean sellcar(Person p):把自己全部的車子賣給別人
boolean buyCar(Car car,Person p):自動查找賣車的人p是否有買主想要買的車car,如果有就買,并返回true, 否則返回false
viod addCar(car):把某輛車送給方法的調用者。
String toString():得到人的信息
屬性/方法:
String ID:ID 車牌號
cost:價格
color:顏色
Person owner:車子的擁有者
to String():得到汽車的信息
equals():比較車子是否同一倆汽車,ID相同則認為相同
屬性/方法:
HashMap people:存放交易人員的信息。Key為身份證號,value為Person對象
static boolean sellCar(Person p1 ,Car car1, Person p2):p1 將 car1 賣給 p2 。并在該方法中記錄效益人的信息到 people 中。
屬性/方法:
Id:號碼
equals():比較兩個身份證是否同一個,ID相同則認為相同
hashCode():正確編寫 hashCode 方法
場景:
一個叫Bob的人:身份證:310 現金:30000。
有一輛車子:ID:001,紅色,價格:50000的車子;
一個叫 Tom 的人:身份證: 210 現金: 70000,
有一輛車子:顏色:白色,ID:003,價格:25000。
一個叫 King 的人:身份證: 245 現金: 60000,
有2輛車子:顏色:白色,ID:005,價格:18000。
顏色:紅色,ID:045,價格:58000。
Tom買了 Bob的車子.他就擁有了 2輛汽車
King 把 ID=005 的車子買給了 Bob
最后各人的信息如何?
3 附錄:排序
為了用’'集合框架"的額外部分把排序支持添加到Java 2 SDK,版本1.2,核心Java庫 作了許多更改。像 String 和 Integer 類如今實現 Comparable 接口以提供自然排序 順序。對于那些沒有自然順序的類、或者當您想要一個不同于自然順序的順序時,您可以實 現 Comparator 接口來定義您自己的。
為了利用排序功能, '集合框架"提供了兩種使用該功能的接口: SortedSet 和 SortedMap 。
Comparable 接口
在 java.lang 包中, Comparable 接口適用于一個類有自然順序的時候。假定對象集合 是同一類型,該接口允許您把集合排序成自然順序。
compareTo() 方法比較當前實例和作為參數傳入的元素。如果排序過程中當前實例出現在參數前,就返回某個負值。如果當前實例出現在參數后,則返回正值。否則,返回零。這 里不要求零返回值表示元素相等。零返回值只是表示兩個對象排在同一個位置。
在Java2SDK,版本1.2中有十四個類實現Comparable接口。下表展示了它們的自 然排序。雖然一些類共享同一種自然排序,但只有相互可比的類才能排序。
創建您自己的類 Comparable 只是個實現 compareTo() 方法的問題。通常就是依賴幾 個數據成員的自然排序。您自己的類也應該覆蓋 equals() 和 hashCode() 以確保兩個 相等的對象返回同一個散列碼。
Comparator 接口
若一個類不能用于實現java .lang .Comparable,您可以提供自己的 java.util.Comparator 行為。如果您不喜歡缺省的 Comparable 行為,您照樣可以 提供自己的 Comparator。
Comparator 的 compare() 方法的返回值和 Comparable 的 compareTo() 方法的 返回值相似。在此情況下,如果排序時第一個元素出現在第二個元素之前,則返回一個負值。 如果第一個元素出現在后,那么返回一個正值。否則,返回零。與 Comparable 相似,零 返回值不表示元素相等。一個零返回值只是表示兩個對象排在同一位置。由 Comparator 用戶決定如何處理。如果兩個不相等的元素比較的結果為零,您首先應該確信那就是您要的結果,然后記錄行為。
為了演示,您會發現編寫一個新的忽略大小寫的Comparator,代替使用Collator進行語言環境特定、忽略大小寫的比較會更容易。這樣的一種實現如下所示:
class CaseInsensitiveComparator implements Comparator { public int compare(Object element1, Object element2) { String lowerE1 = ((String)element1).toLowerCase(); String lowerE2 = ((String)element2).toLowerCase(); return lowerE1.compareTo(lowerE2); } }因為每個類在某些地方都建立了 Object 子類,所以這不是您實現 equals() 方法的必 要條件。實際上大多數情況下您不會去這樣做。切記該equals() 方法檢查的是 Comparator 實現的等同性,不是處于比較狀態下的對象。
Collections類有個預定義的 Comparator 用于重用。調用Collections.reverseOrder() 返回一個 Comparator, 它對逆序實現 Comparable 接口的對象進行排序。
總結
以上是生活随笔為你收集整理的Java集合类原理详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大连房屋登记备案查询系统(大连房屋登记备
- 下一篇: Java的超类/基类Object