1 集合框架
1.1 集合框架概述
1.1.1 容器簡介
到目前為止,我們已經學習了如何創建多個不同的對象,定義了這些對象以后,我們就可以利用它們來做一些有意義的事情。
舉例來說,假設要存儲許多雇員,不同的雇員的區別僅在於雇員的身份證號。我們可以通過身份證號來順序存儲每個雇員,但是在內存中實現呢?是不是要准備足夠的內存來存儲1000個雇員,然后再將這些雇員逐一插入?如果已經插入了500條記錄,這時需要插入一個身份證號較低的新雇員,該怎么辦呢?是在內存中將500條記錄全部下移后,再從開頭插入新的記錄? 還是創建一個映射來記住每個對象的位置?當決定如何存儲對象的集合時,必須考慮如下問題。
對於對象集合,必須執行的操作主要以下三種:
u 添加新的對象
u 刪除對象
u 查找對象
我們必須確定如何將新的對象添加到集合中。可以將對象添加到集合的末尾、開頭或者中間的某個邏輯位置。
從集合中刪除一個對象后,對象集合中現有對象會有什么影響呢?可能必須將內存移來移去,或者就在現有對象所駐留的內存位置下一個“洞”。
在內存中建立對象集合后,必須確定如何定位特定對象。可建立一種機制,利用該機制可根據某些搜索條件(例如身份證號)直接定位到目標對象;否則,便需要遍歷集合中的每個對象,直到找到要查找的對象為止。
前面大家已經學習過了數組。數組的作用是可以存取一組數據。但是它卻存在一些缺點,使得無法使用它來比較方便快捷的完成上述應用場景的要求。
1. 首先,在很多數情況下面,我們需要能夠存儲一組數據的容器,這一點雖然數組可以實現,但是如果我們需要存儲的數據的個數多少並不確定。比如說:我們需要在容器里面存儲某個應用系統的當前的所有的在線用戶信息,而當前的在線用戶信息是時刻都可能在變化的。 也就是說,我們需要一種存儲數據的容器,它能夠自動的改變這個容器的所能存放的數據數量的大小。這一點上,如果使用數組來存儲的話,就顯得十分的笨拙。
2. 我們再假設這樣一種場景:假定一個購物網站,經過一段時間的運行,我們已經存儲了一系列的購物清單了,購物清單中有商品信息。如果我們想要知道這段時間里面有多少種商品被銷售出去了。那么我們就需要一個容器能夠自動的過濾掉購物清單中的關於商品的重復信息。如果使用數組,這也是很難實現的。
3. 最后再想想,我們經常會遇到這種情況,我知道某個人的帳號名稱,希望能夠進一步了解這個人的其他的一些信息。也就是說,我們在一個地方存放一些用戶信息,我們希望能夠通過用戶的帳號來查找到對應的該用戶的其他的一些信息。再舉個查字典例子:假設我們希望使用一個容器來存放單詞以及對於這個單詞的解釋,而當我們想要查找某個單詞的意思的時候,能夠根據提供的單詞在這個容器中找到對應的單詞的解釋。如果使用數組來實現的話,就更加的困難了。
為解決這些問題,Java里面就設計了容器集合,不同的容器集合以不同的格式保存對象。
數學背景
在常見用法中,集合(collection)和數學上直觀的集(set)的概念是相同的。集是一個唯一項組,也就是說組中沒有重復項。實際上,“集合框架”包含了一個Set 接口和許多具體的Set 類。但正式的集概念卻比 Java 技術提前了一個世紀,那時英國數學家 George Boole 按邏輯正式的定義了集的概念。大部分人在小學時通過我們熟悉的維恩圖引入的“集的交”和“集的並”學到過一些集的理論。
集的基本屬性如下:
u 集內只包含每項的一個實例
u 集可以是有限的,也可以是無限的
u 可以定義抽象概念
集不僅是邏輯學、數學和計算機科學的基礎,對於商業和系統的日常應用來說,它也很實用。“連接池”這一概念就是數據庫服務器的一個開放連接集。Web 服務器必須管理客戶機和連接集。文件描述符提供了操作系統中另一個集的示例。
映射是一種特別的集。它是一種對(pair)集,每個對表示一個元素到另一元素的單向映射。一些映射示例有:
u IP 地址到域名(DNS)的映射
u 關鍵字到數據庫記錄的映射
u 字典(詞到含義的映射)
u 2 進制到 10 進制轉換的映射
就像集一樣,映射背后的思想比 Java 編程語言早的多,甚至比計算機科學還早。而Java中的Map 就是映射的一種表現形式。
1.1.2 容器的分類
既然您已經具備了一些集的理論,您應該能夠更輕松的理解“集合框架”。“集合框架”由一組用來操作對象的接口組成。不同接口描述不同類型的組。在很大程度上,一旦您理解了接口,您就理解了框架。雖然您總要創建接口特定的實現,但訪問實際集合的方法應該限制在接口方法的使用上;因此,允許您更改基本的數據結構而不必改變其它代碼。框架接口層次結構如下圖所示。
Java容器類類庫的用途是“保存對象”,並將其划分為兩個不同的概念:
1) Collection 。 一組對立的元素,通常這些元素都服從某種規則。List必須保持元素特定的順序,而Set 不能有重復元素。
2) Map 。 一組 成對的“鍵值對”對象。初看起來這似乎應該是一個Collection ,其元素是成對的對象,但是這樣的設計實現起來太笨拙了,於是我們將Map明確的提取出來形成一個獨立的概念。另一方面,如果使用Collection 表示Map的部分內容,會便於查看此部分內容。因此Map一樣容易擴展成多維Map ,無需增加新的概念,只要讓Map中的鍵值對的每個“值”也是一個Map即可。
Collection和Map的區別在於容器中每個位置保存的元素個數。Collection 每個位置只能保存一個元素(對象)。此類容器包括:List ,它以特定的順序保存一組元素;Set 則是元素不能重復。
Map保存的是“鍵值對”,就像一個小型數據庫。我們可以通過“鍵”找到該鍵對應的“值”。
u Collection – 對象之間沒有指定的順序,允許重復元素。
u Set – 對象之間沒有指定的順序,不允許重復元素
u List– 對象之間有指定的順序,允許重復元素,並引入位置下標。
u Map – 接口用於保存關鍵字(Key)和數值(Value)的集合,集合中的每個對象加入時都提供數值和關鍵字。Map 接口既不繼承 Set 也不繼承 Collection。
List、Set、Map共同的實現基礎是Object數組
除了四個歷史集合類外,Java 2 框架還引入了六個集合實現,如下表所示。
接口 |
實現 |
歷史集合類 |
Set |
HashSet |
|
|
TreeSet |
|
List |
ArrayList |
Vector |
|
LinkedList |
Stack |
Map |
HashMap |
Hashtable |
|
TreeMap |
Properties |
這里沒有 Collection 接口的實現,接下來我們再來看一下下面的這張關於集合框架的大圖:
這張圖看起來有點嚇人,熟悉之后就會發現其實只有三種容器:Map,List和Set ,它們各自有兩個三個實現版本。常用的容器用黑色粗線框表示。
點線方框代表“接口”,虛線方框代表抽象類,而實線方框代表普通類(即具體類,而非抽象類)。虛線箭頭指出一個特定的類實現了一個接口(在抽象類的情況下,則是“部分”實現了那個接口)。實線箭頭指出一個類可生成箭頭指向的那個類的對象。例如任何集合( Collection )都能產生一個迭代器( Iterator ),而一個List 除了能生成一個ListIterator (列表迭代器)外,還能生成一個普通迭代器,因為List 正是從集合繼承來的.
1.2 Collection
1.2.1 常用方法
Collection
接口用於表示任何對象或元素組。想要盡可能以常規方式處理一組元素時,就使用這一接口。Collection 在前面的大圖也可以看出,它是List和Set 的父類。並且它本身也是一個接口。它定義了作為集合所應該擁有的一些方法。如下:
注意:
集合必須只有對象,集合中的元素不能是基本數據類型。
Collection接口支持如添加和除去等基本操作。設法除去一個元素時,如果這個元素存在,除去的僅僅是集合中此元素的一個實例。
u booleanadd(Object element)
u booleanremove(Object element)
Collection接口還支持查詢操作:
u int size()
u booleanisEmpty()
u booleancontains(Object element)
u Iteratoriterator()
組操作 :Collection 接口支持的其它操作,要么是作用於元素組的任務,要么是同時作用於整個集合的任務。
u boolean containsAll(Collectioncollection)
u boolean addAll(Collection collection)
u void clear()
u void removeAll(Collection collection)
u void retainAll(Collection collection)
containsAll() 方法允許您查找當前集合是否包含了另一個集合的所有元素,即另一個集合是否是當前集合的子集。其余方法是可選的,因為特定的集合可能不支持集合更改。 addAll() 方法確保另一個集合中的所有元素都被添加到當前的集合中,通常稱為並。 clear() 方法從當前集合中除去所有元素。 removeAll() 方法類似於 clear() ,但只除去了元素的一個子集。 retainAll() 方法類似於 removeAll() 方法,不過可能感到它所做的與前面正好相反:它從當前集合中除去不屬於另一個集合的元素,即交。
我們看一個簡單的例子,來了解一下集合類的基本方法的使用:
import java.util.*;
public class CollectionToArray {
public static voidmain(String[] args) {
Collection collection1=newArrayList();//創建一個集合對象
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("集合collection1中是否包含111 :"+collection1.contains("111"));
Collection collection2=newArrayList();
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(inti=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數組的方法:
u Object[] toArray()
u 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 能夠用來,:
1) 使用方法 iterator() 要求容器返回一個Iterator .第一次調用Iterator 的next() 方法時,它返回集合序列的第一個元素。
2) 使用next() 獲得集合序列的中的下一個元素。
3) 使用hasNext()檢查序列中是否元素。
4) 使用remove()將迭代器新返回的元素刪除。
需要注意的是:方法刪除由next方法返回的最后一個元素,在每次調用next時,remove方法只能被調用一次 。
大家看,Java 實現的這個迭代器的使用就是如此的簡單。Iterator(跌代器)雖然功能簡單,但仍然可以幫助我們解決許多問題,同時針對List 還有一個更復雜更高級的ListIterator。您可以在下面的List講解中得到進一步的介紹。
1.3 List
1.3.1 概述
前面我們講述的Collection[i1] 接口實際上並沒有直接的實現類。而List是容器的一種,表示列表的意思。當我們不知道存儲的數據有多少的情況,我們就可以使用List 來完成存儲數據的工作。例如前面提到的一種場景。我們想要在保存一個應用系統當前的在線用戶的信息。我們就可以使用一個List來存儲。因為List的最大的特點就是能夠自動的根據插入的數據量來動態改變容器的大小。下面我們先看看List接口的一些常用方法。
1.3.2 常用方法
List就是列表的意思,它是Collection 的一種,即
繼承了 Collection
接口,以定義一個允許重復項的有序集合。該接口不但能夠對列表的一部分進行處理,還添加了面向位置的操作。List是按對象的進入順序進行保存對象,而不做排序或編輯操作。它除了擁有Collection接口的所有的方法外還擁有一些其他的方法。
面向位置的操作包括插入某個元素或 Collection 的功能,還包括獲取、除去或更改元素的功能。在 List 中搜索元素可以從列表的頭部或尾部開始,如果找到元素,還將報告元素所在的位置。
u void add(intindex, Object element) :添加對象element到位置index上
u booleanaddAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素
u Object get(intindex) :取出下標為index的位置的元素
u intindexOf(Object element) :查找對象element 在List中第一次出現的位置
u intlastIndexOf(Object element) :查找對象element 在List中最后出現的位置
u Objectremove(int index) :刪除index位置上的元素
u Object set(intindex, Object element) :將index位置上的對象替換為element 並返回老的元素。
先看一下下面表格:
|
簡述 |
實現 |
操作特性 |
成員要求 |
List |
提供基於索引的對成員的隨機訪問 |
ArrayList |
提供快速的基於索引的成員訪問,對尾部成員的增加和刪除支持較好 |
成員可為任意Object子類的對象 |
LinkedList |
對列表中任何位置的成員的增加和刪除支持較好,但對基於索引的成員訪問支持性能較差 |
成員可為任意Object子類的對象 |
在“集合框架”中有兩種常規的List實現:ArrayList和LinkedList。使用兩種 List實現的哪一種取決於您特定的需要。如果要支持隨機訪問,而不必在除尾部的任何位置插入或除去元素,那么,ArrayList提供了可選的集合。但如果,您要頻繁的從列表的中間位置添加和除去元素,而只要順序的訪問列表元素,那么,LinkedList實現更好。
我們以ArrayList 為例,先看一個簡單的例子:
例子中,我們把12個月份存放到ArrayList中,然后用一個循環,並使用get()方法將列表中的對象都取出來。
而LinkedList添加了一些處理列表兩端元素的方法(下圖只顯示了新方法):
使用這些新方法,您就可以輕松的把 LinkedList 當作一個堆棧、隊列或其它面向端點的數據結構。
我們再來看另外一個使用LinkedList 來實現一個簡單的隊列的例子:
import java.util.*;
public class ListExample {
public static void main(String args[]) {
LinkedList queue = new LinkedList();
queue.addFirst("Bernadine");
queue.addFirst("Elizabeth");
queue.addFirst("Gene");
queue.addFirst("Elizabeth");
queue.addFirst("Clara");
System.out.println(queue);
queue.removeLast();
queue.removeLast();
System.out.println(queue);
}
}
運行程序產生了以下輸出。請注意,與Set不同的是List允許重復。
[Clara, Elizabeth, Gene,Elizabeth, Bernadine]
[Clara, Elizabeth, Gene]
該的程序演示了具體 List類的使用。第一部分,創建一個由 ArrayList 支持的List。填充完列表以后,特定條目就得到了。示例的LinkedList部分把 LinkedList 當作一個隊列,從隊列頭部添加東西,從尾部除去。
List接口不但以位置友好的方式遍歷整個列表,還能處理集合的子集:
u ListIterator listIterator() :返回一個ListIterator跌代器,默認開始位置為0
u ListIterator listIterator(int startIndex) :返回一個ListIterator跌代器,開始位置為startIndex
u 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。
List list = ...;
ListIterator iterator = list.listIterator(list.size());
while (iterator.hasPrevious()) {
Object element = iterator.previous();
// Process element
}
正常情況下,不用 ListIterator
改變某次遍歷集合元素的方向 — 向前或者向后。雖然在技術上可能實現時,但在 previous()
后立刻調用 next()
,返回的是同一個元素。把調用next() 和 previous() 的順序顛倒一下,結果相同。
我們看一個List的例子:
import java.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() Constructs an empty list with an initial capacity of ten. |
ArrayList(Collection c) Constructs a list containing the elements of the specified collection, in the order they are returned by the collection's iterator. |
ArrayList(int initialCapacity) Constructs an empty list with the specified initial capacity. |
其中第一個構造函數ArrayList()和第二構造函數ArrayList(Collection c) 是按照Collection 接口文檔所述,所應該提供兩個構造函數,一個無參數,一個接受另一個 Collection。
第3個構造函數:
ArrayList(int initialCapacity) 是ArrayList實現的比較重要的構造函數,雖然,我們不常用它,但是某認的構造函數正是調用的該帶參數:initialCapacity 的構造函數來實現的。 其中參數:initialCapacity 表示我們構造的這個ArrayList列表的初始化容量是多大。如果調用默認的構造函數,則表示默認調用該參數為initialCapacity =10 的方式,來進行構建一個ArrayList列表對象。
為了更好的理解這個initialCapacity 參數的概念,我們先看看ArrayList在Sun 提供的源碼中的實現方式。先看一下它的屬性有哪些:
ArrayList 繼承了AbstractList 我們主要看看ArrayList中的屬性就可以了。
ArrayList中主要包含2個屬性:
u private transient ObjectelementData[];
u private int size;
其中數組::elementData[]是列表的實現核心屬性:數組。 我們使用該數組來進行存放集合中的數據。而我們的初始化參數就是該數組構建時候的長度,即該數組的length屬性就是initialCapacity 參數。
Keys:transient表示被修飾的屬性不是對象持久狀態的一部分,不會自動的序列化。
第2個屬性:size表示列表中真實數據的存放個數。
我們再來看一下ArrayList的構造函數,加深一下ArrayList是基於數組的理解。
從源碼中可以看到默認的構造函數調用的就是帶參數的構造函數:
publicArrayList(int initialCapacity)
不過參數initialCapacity=10 。
我們主要看ArrayList(int initialCapacity) 這個構造函數。可以看到:
this.elementData= new Object[initialCapacity];
我們就是使用的initialCapacity這個參數來創建一個Object數組。而我們所有的往該集合對象中存放的數據,就是存放到了這個Object數組中去了。
我們在看看另外一個構造函數的源碼:
這里,我們先看size() 方法的實現形式。它的作用即是返回size屬性值的大小。然后我們再看另外一個構造函數public ArrayList(Collection c) ,該構造函數的作用是把另外一個容器對象中的元素存放到當前的List 對象中。
可以看到,首先,我們是通過調用另外一個容器對象C 的方法size()來設置當前的List對象的size屬性的長度大小。
接下來,就是對elementData 數組進行初始化,初始化的大小為原先容器大小的1.1倍。最后,就是通過使用容器接口中的Object[] toArray(Object[] a) 方法來把當前容器中的對象都存放到新的數組elementData 中。這樣就完成了一個ArrayList 的建立。
可能大家會存在一個問題,那就是,我們建立的這個ArrayList 是使用數組來實現的,但是數組的長度一旦被定下來,就不能改變了。而我們在給ArrayList對象中添加元素的時候,卻沒有長度限制。這個時候,ArrayList 中的elementData 屬性就必須存在一個需要動態的擴充容量的機制。我們看下面的代碼,它描述了這個擴充機制:
這個方法的作用就是用來判斷當前的數組是否需要擴容,應該擴容多少。其中屬性:modCount是繼承自父類,它表示當前的對象對elementData數組進行了多少次擴容,清空,移除等操作。該屬性相當於是一個對於當前List 對象的一個操作記錄日志號。 我們主要看下面的代碼實現:
1. 首先得到當前elementData 屬性的長度oldCapacity。
2. 然后通過判斷oldCapacity和minCapacity參數誰大來決定是否需要擴容
n 如果minCapacity大於oldCapacity,那么我們就對當前的List對象進行擴容。擴容的的策略為:取(oldCapacity * 3)/2 + 1和minCapacity之間更大的那個。然后使用數組拷貝的方法,把以前存放的數據轉移到新的數組對象中
n 如果minCapacity不大於oldCapacity那么就不進行擴容。
下面我們看看上的那個ensureCapacity方法的是如何使用的:
上的兩個add方法都是往List 中添加元素。每次在添加元素的時候,我們就需要判斷一下,是否需要對於當前的數組進行擴容。
我們主要看看 public booleanadd(Object o)方法,可以發現在添加一個元素到容器中的時候,首先我們會判斷是否需要擴容。因為只增加一個元素,所以擴容的大小判斷也就為當前的size+1來進行判斷。然后,就把新添加的元素放到數組elementData中。
第二個方法publicboolean addAll(Collection c)也是同樣的原理。將新的元素放到elementData數組之后。同時改變當前List 對象的size屬性。
類似的List 中的其他的方法也都是基於數組進行操作的。大家有興趣可以看看源碼中的更多的實現方式。
最后我們再看看如何判斷在集合中是否已經存在某一個對象的:
由源碼中我們可以看到,public boolean contains(Object elem)方法是通過調用public int indexOf(Object elem)方法來判斷是否在集合中存在某個對象elem。我們看看indexOf方法的具體實現。
u 首先我們判斷一下elem 對象是否為null,如果為null的話,那么遍歷數組elementData 把第一個出現null的位置返回。
u 如果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 作為一個鍵或值添加給自身。
u
Object put(Object key,Object value)
:用來存放一個鍵
-
值對
Map
中
u
Object remove(Object key)
:根據
key(
鍵
)
,移除一個鍵
-
值對,並將值返回
u
voidputAll(Map mapping)
:將另外一個
Map
中的元素存入當前的
Map
中
u
void clear()
:清空當前
Map
中的元素
查詢操作允許您檢查映射內容:
u
Object get(Object key)
:根據
key(
鍵
)
取得對應的值
u
boolean containsKey(Object key)
:判斷
Map
中是否存在某鍵(
key
)
u
boolean containsValue(Object value):
判斷
Map
中是否存在某值
(value)
u
int size():
返回
Map
中
鍵-值對的個數
u
boolean isEmpty()
:判斷當前
Map
是否為空
最后一組方法允許您把鍵或值的組作為集合來處理。
u
publicSet keySet()
:返回所有的鍵(
key
),並使用
Set
容器存放
u
public Collection values()
:返回所有的值(
Value
),並使用
Collection
存放
u
publicSet entrySet()
:
返回一個實現Map.Entry接口的元素Set
因為映射中鍵的集合必須是唯一的,就使用 Set 來支持。因為映射中值的集合可能不唯一,就使用 Collection 來支持。最后一個方法返回一個實現 Map.Entry 接口的元素 Set。
我們看看Map的常用實現類的比較,如下表:
|
簡述 |
實現 |
操作特性 |
成員要求 |
Map |
保存鍵值對成員,基於鍵找值操作,使用compareTo或compare方法對鍵進行排序 |
HashMap |
能滿足用戶對Map的通用需求 |
鍵成員可為任意Object子類的對象,但如果覆蓋了equals方法,同時注意修改hashCode方法。 |
TreeMap |
支持對鍵有序地遍歷,使用時建議先用HashMap增加和刪除成員,最后從HashMap生成TreeMap;附加實現了SortedMap接口,支持子Map等要求順序的操作 |
鍵成員要求實現Comparable接口,或者使用Comparator構造TreeMap鍵成員一般為同一類型。 |
||
LinkedHashMap |
保留鍵的插入順序,用equals 方法檢查鍵和值的相等性 |
成員可為任意Object子類的對象,但如果覆蓋了equals方法,同時注意修改hashCode方法。 |
下面我們看一個簡單的例子:
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 就實現了該接口。我們可以看一下這個類的源碼:
可以看到compareTo 方法里面通過判斷當前的Integer對象的值是否大於傳入的參數的值來得到返回值的。
在 Java 2 SDK,版本 1.2 中有十四個類實現 Comparable 接口。下表展示了它們的自然排序。雖然一些類共享同一種自然排序,但只有相互可比的類才能排序。
類 |
排序 |
BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short |
按數字大小排序 |
Character |
按 Unicode 值的數字大小排序 |
CollationKey |
按語言環境敏感的字符串排序 |
Date |
按年代排序 |
File |
按系統特定的路徑名的全限定字符的 Unicode 值排序 |
ObjectStreamField |
按名字中字符的 Unicode 值排序 |
String |
按字符串中字符 Unicode 值排序 |
這里只是簡單的介紹一下排序接口,如果要詳細的了解排序部分內容的話,可以參考文章最后的附錄部分對於排序的更加詳細的描述。
我們再回到Map中來,Java提高的API中除了上面介紹的幾種Map比較常用以為還有一些Map,大家可以了解一下:
u WeakHashMap: WeakHashMap 是 Map 的一個特殊實現,它只用於存儲對鍵的弱引用。當映射的某個鍵在 WeakHashMap 的外部不再被引用時,就允許垃圾收集器收集映射中相應的鍵值對。使用 WeakHashMap 有益於保持類似注冊表的數據結構,其中條目的鍵不再能被任何線程訪問時,此條目就沒用了。
u IdentifyHashMap: Map的一種特性實現,關鍵屬性的hash碼不是由hashCode()方法計算,而是由System.identityHashCode 方法計算,使用==進行比較而不是equals()方法。
通過簡單的對與Map中各個常用實現類的使用,為了更好的理解Map,下面我們再來了解一下Map的實現原理。
有的人可能會認為 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,然后我們分別在該表中的格子中存放不同的元素。每個格子下面存放的元素又是以鏈表的方式存放元素。
u 當添加一個新的元素Entry的時候,首先我們通過一個Hash函數計算出這個Entry元素的Hash值hashcode。通過該hashcode值,就可以直接定位出我們應該把這個Entry元素存入到Hash表的哪個格子中,如果該格子中已經存在元素了,那么只要把新的Entry元存放到這個鏈表中即可。
u 如果要查找一個元素Entry的時候,也同樣的方式,通過Hash函數計算出這個Entry元素的Hash值hashcode。然后通過該hashcode值,就可以直接找到這個Entry是存放到哪個格子中的。接下來就對該格子存放的鏈表元素進行逐個的比較查找就可以了。
舉一個比較簡單的例子來說明這個算法的運算方式:
假定我們有一個長度為8的Hash表(可以理解為一個長度為8的數組)。在這個Hash表中存放數字:如下表
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
假定我們的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表中的哪個對應數字的格子中。如果改格子中已經有數字存在了,那么就以鏈表的方式將數字依次存放在該格子中,如下表:
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
32 |
9 |
|
11 |
44 |
45 |
6 |
23 |
|
57 |
|
|
12 |
|
|
|
|
89 |
|
|
|
|
|
|
Hash表和Hash算法的特點就是它的存取速度比數組差一些,但是比起單純的鏈表,在查找和存儲方面卻要好很多。同時數組也不利於數據的重構而排序等方面的要求。
更具體的說明,讀者可以參考數據結構相關方面的書籍。
簡單的了解了一下Hash算法后,我們就來看看HashMap的屬性有哪些:
里面最重要的3個屬性:
transientEntry[] table: 用來存放鍵值對的對象Entry數組,也就是Hash表
transient int size:當前Map中存放的鍵值對的個數
final float loadFactor:負載因子,用來決定什么情況下應該對Entry進行擴容
我們Entry 對象是Map接口中的一個內部接口。即是使用它來保存我們的鍵值對的。
我們看看這個Entry 內部接口在HashMap中的實現:
通過查看源碼,我們可以看到Entry類有點類似一個單向鏈表。其中:
final Object key 和 Object value;存放的就是我們放入Map中的鍵值對。
而屬性Entry next;表示當前鍵值對的下一個鍵值對是哪個Entry。
接下來,我們看看HashMap的主要的構造函數:
我們主要看看 public HashMap(int initialCapacity, float loadFactor)
因為,另外兩個構造函數實行也是同樣的方式進行構建一個HashMap 的。
該構造函數:
1. 首先是判斷參數intinitialCapacity和float loadFactor是否合法
2. 然后確定Hash表的初始化長度。確定的策略是:通過傳進來的參數initialCapacity 來找出第一個大於它的2的次方的數。比如說我們傳了18這樣的一個initialCapacity 參數,那么真實的table數組的長度為2的5次方,即32。之所以采用這種策略來構建Hash表的長度,是因為2的次方的運算對於現代的處理器來說,可以通過一些方法得到更加好的執行效率。
3. 接下來就是得到重構因子(threshold)了,這個屬性也是HashMap中的一個比較重要的屬性,它表示,當Hash表中的元素被存放了多少個之后,我們就需要對該Hash表進行重構。
4. 最后就是使用得到的初始化參數capacity 來構建Hash表:Entry[] table。
下面我們看看一個鍵值對是如何添加到HashMap中的。
該put方法是用來添加一個鍵值對(key-value)到Map中,如果Map中已經存在相同的鍵的鍵值對的話,那么就把新的值覆蓋老的值,並把老的值返回給方法的調用者。如果不存在一樣的鍵,那么就返回null 。我們看看方法的具體實現:
1. 首先我們判斷如果key為null則使用一個常量來代替該key值,該行為在方法maskNull()終將key替換為一個非null 的對象k。
2. 計算key值的Hash碼:hash
3. 通過使用Hash碼來定位,我們應該把當前的鍵值對存放到Hash表中的哪個格子中。indexFor()方法計算出的結果:i 就是Hash表(table)中的下標。
4. 然后遍歷當前的Hash表中table[i]格中的鏈表。從中判斷已否已經存在一樣的鍵(Key)的鍵值對。如果存在一樣的key的鍵,那么就用新的value覆寫老的value,並把老的value返回
5. 如果遍歷后發現沒有存在同樣的鍵值對,那么就增加當前鍵值對到Hash表中的第i個格子中的鏈表中。並返回null 。
最后我們看看一個鍵值對是如何添加到各個格子中的鏈表中的:
我們先看void addEntry(int hash, Object key, Object value, int bucketIndex)方法,該方法的作用就用來添加一個鍵值對到Hash表的第bucketIndex個格子中的鏈表中去。這個方法作的工作就是:
1. 創建一個Entry對象用來存放鍵值對。
2. 添加該鍵值對 ---- Entry對象到鏈表中
3. 最后在size屬性加一,並判斷是否需要對當前的Hash表進行重構。如果需要就在void resize(int newCapacity)方法中進行重構。
之所以需要重構,也是基於性能考慮。大家可以考慮這樣一種情況,假定我們的Hash表只有4個格子,那么我們所有的數據都是放到這4個格子中。如果存儲的數據量比較大的話,例如100。這個時候,我們就會發現,在這個Hash表中的4個格子存放的4個長長的鏈表。而我們每次查找元素的時候,其實相當於就是遍歷鏈表了。這種情況下,我們用這個Hash表來存取數據的性能實際上和使用鏈表差不多了。
但是如果我們對這個Hash表進行重構,換為使用Hash表長度為200的表來存儲這100個數據,那么平均2個格子里面才會存放一個數據。這個時候我們查找的數據的速度就會非常的快。因為基本上每個格子中存放的鏈表都不會很長,所以我們遍歷鏈表的次數也就很少,這樣也就加快了查找速度。但是這個時候又存在了另外的一個問題。我們使用了至少200個數據的空間來存放100個數據,這樣就造成至少100個數據空間的浪費。 在速度和空間上面,我們需要找到一個適合自己的中間值。在HashMap中我們通過負載因子(loadFactor)來決定應該什么時候應該重構我們的Hash表,以達到比較好的性能狀態。
我們再看看重構Hash表的方法:void resize(int newCapacity)是如何實現的:
它的實現方式比較簡單:
1. 首先判斷如果Hash表的長度已經達到最大值,那么就不進行重構了。因為這個時候Hash表的長度已經達到上限,已經沒有必要重構了。
2. 然后就是構建新的Hash表
3. 把老的Hash表中的對象數據全部轉移到新的Hash表newTable中,並設置新的重構因子threshold
對於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的情況:
package c08.hashEx;
import java.util.*;
//身份證類
class Code{
final int id;//身份證號碼已經確認,不能改變
Code(int i){
id=i;
}
//身份號號碼相同,則身份證相同
public boolean equals(Object anObject) {
if (anObject instanceof Code){
Code other=(Code) anObject;
return this.id==other.id;
}
return false;
}
public String toString() {
return "身份證:"+id;
}
}
//人員信息類
class Person {
Code id;// 身份證
String name;// 姓名
public Person(String name, Code id) {
this.id=id;
this.name=name;
}
//如果身份證號相同,就表示兩個人是同一個人
public boolean equals(Object anObject) {
if (anObject instanceof Person){
Person other=(Person)anObject;
return this.id.equals(other.id);
}
return false;
}
public String toString() {
return "姓名:"+name+" 身份證:"+id.id+"\n";
}
}
public class HashCodeEx {
public static void main(String[] args) {
HashMap map=new HashMap();
Person p1=new Person("張三",new Code(123));
map.put(p1.id,p1);//我們根據身份證來作為key值存放到Map中
Person p2=new Person("李四",new Code(456));
map.put(p2.id,p2);
Person p3=new Person("王二",new Code(789));
map.put(p3.id,p3);
System.out.println("HashMap 中存放的人員信息:\n"+map);
// 張三改名為:張山但是還是同一個人。
Person p4=new Person("張山",new Code(123));
map.put(p4.id,p4);
System.out.println("張三改名后 HashMap 中存放的人員信息:\n"+map);
//查找身份證為:123 的人員信息
System.out.println("查找身份證為:123 的人員信息:"+map.get(new Code(123)));
}
}
運行結果為:
HashMap 中存放的人員信息:
{身份證:456=姓名:李四 身份證:456
, 身份證:123=姓名:張三身份證:123
, 身份證:789=姓名:王二 身份證:789
}
張三改名后 HashMap 中存放的人員信息:
{身份證:456=姓名:李四 身份證:456
, 身份證:123=姓名:張三身份證:123
, 身份證:123=姓名:張山身份證:123
, 身份證:789=姓名:王二 身份證:789
}
查找身份證為:123 的人員信息:null
上面的例子的演示的是,我們在一個HashMap中存放了一些人員的信息。並以這些人員的身份證最為人員的“鍵”。當有的人員的姓名修改了的情況下,我們需要更新這個HashMap。同時假如我們知道某個身份證號,想了解這個身份證號對應的人員信息如何,我們也可以根據這個身份證號在HashMap中得到對應的信息。
而例子的輸出結果表示,我們所做的更新和查找操作都失敗了。失敗的原因就是我們的身份證類:Code 沒有覆寫hashCode()方法。這個時候,當查找一樣的身份證號碼的鍵值對的時候,使用的是默認的對象的內存地址來進行定位。這樣,后面的所有的身份證號對象new Code(123) 產生的hashCode()值都是不一樣的。所以導致操作失敗。
下面,我們給Code類加上hashCode()方法,然后再運行一下程序看看:
//身份證類
class Code{
final int id;//身份證號碼已經確認,不能改變
Code(int i){
id=i;
}
//身份號號碼相同,則身份證相同
public boolean equals(Object anObject) {
if (anObject instanceof Code){
Code other=(Code) anObject;
return this.id==other.id;
}
return false;
}
public String toString() {
return "身份證:"+id;
}
//覆寫hashCode方法,並使用身份證號作為hash值
public int hashCode(){
return id;
}
}
再次執行上面的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()
方法來檢查等同性。
我們簡單的描述一下各個方法的作用:
u public int size() :返回set中元素的數目,如果set包含的元素數大於Integer.MAX_VALUE,返回Integer.MAX_VALUE
u public boolean isEmpty() :如果set中不含元素,返回true
u public booleancontains(Object o) :如果set包含指定元素,返回true
u public Iterator iterator()
l 返回set中元素的迭代器
l 元素返回沒有特定的順序,除非set是提高了該保證的某些類的實例
u public Object[] toArray() :返回包含set中所有元素的數組
u public Object[]toArray(Object[] a) :返回包含set中所有元素的數組,返回數組的運行時類型是指定數組的運行時類型
u public boolean add(Object o) :如果set中不存在指定元素,則向set加入
u public boolean remove(Object o) :如果set中存在指定元素,則從set中刪除
u public booleanremoveAll(Collection c) :如果set包含指定集合,則從set中刪除指定集合的所有元素
u public booleancontainsAll(Collection c) :如果set包含指定集合的所有元素,返回true。如果指定集合也是一個set,只有是當前set的子集時,方法返回true
u public booleanaddAll(Collection c) :如果set中中不存在指定集合的元素,則向set中加入所有元素
u public booleanretainAll(Collection c) :只保留set中所含的指定集合的元素(可選操作)。換言之,從set中刪除所有指定集合不包含的元素。 如果指定集合也是一個set,那么該操作修改set的效果是使它的值為兩個set的交集
u public booleanremoveAll(Collection c) :如果set包含指定集合,則從set中刪除指定集合的所有元素
u public void clear() :從set中刪除所有元素
“集合框架” 支持 Set 接口兩種普通的實現:HashSet 和 TreeSet以及LinkedHashSet。下表中是Set的常用實現類的描述:
|
簡述 |
實現 |
操作特性 |
成員要求 |
Set |
成員不能重復 |
HashSet |
外部無序地遍歷成員。 |
成員可為任意Object子類的對象,但如果覆蓋了equals方法,同時注意修改hashCode方法。 |
TreeSet |
外部有序地遍歷成員; 附加實現了SortedSet, 支持子集等要求順序的操作 |
成員要求實現Comparable接口,或者使用Comparator構造TreeSet。成員一般為同一類型。 |
||
LinkedHashSet |
外部按成員的插入順序遍歷成員 |
成員與HashSet成員類似 |
在更多情況下,您會使用 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轉化為數組
Object s[]= set1.toArray();
for(int i=0;i<s.length;i++){
System.out.println(s[i]);
}
}
}
程序執行的結果為:
1 add true
集合set1的大小:4
集合set1的內容:[222, a, 000, 111]
集合set1移除 000 后的內容:[222, a, 111]
集合set1中是否包含000 :false
集合set1中是否包含111 :true
集合set2的內容:[222, a, 111]
集合set2是否為空 :true
iterator = 222
iterator = a
iterator = 111
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的進一步分析大家就能更加清楚的了解這點了。
Java中Set的概念和數學中的集合(set)一致,都表示一個集內可以存放的元素是不能重復的。
前面我們會發現,Set中很多實現類和Map中的一些實現類的使用上非常的相似。而且前面再講解Map的時候,我們也提到:Map中的“鍵值對”,其中的“鍵”是不能重復的。這個和Set中的元素不能重復一致。我們以HashSet為例來分析一下,會發現其實Set利用的就是Map中“鍵”不能重復的特性來實現的。
先看看HashSet中的有哪些屬性:
再結合構造函數來看看:
通過這些方法,我們可以發現,其實HashSet的實現,全部的操作都是基於HashMap來進行的。我們看看是如何通過HashMap來保證我們的HashSet的元素不重復性的:
看到這個操作我們可以發現HashSet的巧妙實現:就是建立一個“鍵值對”,“鍵”就是我們要存入的對象,“值”則是一個常量。這樣可以確保,我們所需要的存儲的信息之是“鍵”。而“鍵”在Map中是不能重復的,這就保證了我們存入Set中的所有的元素都不重復。而判斷是否添加元素成功,則是通過判斷我們向Map中存入的“鍵值對”是否已經存在,如果存在的話,那么返回值肯定是常量:PRESENT ,表示添加失敗。如果不存在,返回值就為null 表示添加成功。
我們再看看其他的方法實現:
了解了這些后,我們就不難理解,為什么HashMap中需要注意的地方,在HashSet中也同樣的需要注意。其他的Set的實現類也是差不多的原理。
至此對於Set我們就應該能夠比較好的理解了。
1.6 總結:集合框架中常用類比較
用“集合框架”設計軟件時,記住該框架四個基本接口的下列層次結構關系會有用處:
- Collection 接口是一組允許重復的對象。
- Set 接口繼承 Collection,但不允許重復。
- List 接口繼承 Collection,允許重復,並引入位置下標。
- Map 接口既不繼承 Set 也不繼承 Collection, 存取的是鍵值對
我們以下面這個圖表來描述一下常用的集合的實現類之間的區別:
接口 |
成員重復性 |
元素存放順序(Ordered/Sorted) |
元素中被調用的方法 |
基於那中數據結構來實現的 |
|
Set |
Unique elements |
No order |
equals() hashCode() |
Hash 表 |
|
LinkedHashSet |
Set |
Unique elements |
Insertion order |
equals() hashCode() |
Hash 表和雙向鏈表 |
SortedSet |
Unique elements |
Sorted |
equals() compareTo() |
平衡樹(Balanced tree) |
|
ArrayList |
List |
Allowed |
Insertion order |
equals() |
數組 |
LinkedList |
List |
Allowed |
Insertion order |
equals() |
鏈表 |
Vector |
List |
Allowed |
Insertion order |
equals() |
數組 |
HashMap |
Map |
Unique keys |
No order |
equals() hashCode() |
Hash 表 |
LinkedHashMap |
Map |
Unique keys |
Key insertion order/Access order of entries |
equals() hashCode() |
Hash 表和雙向鏈表 |
Hashtable |
Map |
Unique keys |
No order |
equals() hashCode() |
Hash 表 |
TreeMap |
SortedMap |
Unique keys |
Sorted in key order |
equals() compareTo() |
平衡樹(Balanced tree) |
版權聲明:本文為小平果原創文章,轉載請注明:http://blog.csdn.net/i10630226