1 Set和儲存順序
加入Set
的元素必須定義equals()
方法以確保對象的唯一性。
hashCode()
只有這個類被置於HashSet
或者LinkedHashSet
中時才是必需的。但是對於良好的編程風格而言,你應該在覆蓋equals()方法時,總是同時覆蓋hashCode()方法。
如果一個對象被用於任何種類的排序容器中,例如SortedSet
(TreeSet
是其唯一實現),那麼它必須實現Comparable
介面。
注意,SortedSet
的意思是“按對象的比較函數對元素排序”,而不是指“元素插入的次序”。插入順序用LinkedHashSet
來儲存。
2 隊列
3 Map
映射表(也稱為關聯陣列Associative Array)。
3.1 效能
HashMap
使用了特殊的值,稱作散列碼(hash code),來取代對鍵的緩慢搜尋。散列碼是“相對唯一”的、用以代表對象的int
值,它是通過將該對象的某些資訊進行轉換而產生的。
hashCode()是根類Object中的方法,因此所有對象都能產生散列碼。
對Map中使用的鍵的要求與對Set中的元素的要求一樣:
4 散列與散列碼
HashMap使用equals()
判斷當前的鍵是否與表中存在的鍵相同。
預設的Object.equals()只是比較對象的地址。如果要使用自己的類作為HashMap的鍵,必須同時重寫hashCode()
和equals()
。
正確的equals()
方法必須滿足下列5個條件:
4.1 散列概念
使用散列的目的在於:想要使用一個對象來尋找另一個對象。
Map的實作類別使用散列是為了提高查詢速度。
散列的價值在於速度:散列使得查詢得以快速進行。由於瓶頸位於查詢速度,因此解決方案之一就是保持鍵的排序狀態,然後使用Collections.binarySearch()
進行查詢。
散列則更進一步,它將鍵儲存在某處,以便能夠很快找到。儲存一組元素最快的資料結構是數組,所以用它來表示鍵的資訊(請小心留意,我是說鍵的資訊,而不是鍵本身)。但是因為數組不能調整容量,因此就有一個問題:我們希望在Map中儲存數量不確定的值,但是如果鍵的數量被數組的容量限制了,該怎麼辦?
答案就是:數組並不儲存鍵本身。而是通過鍵對象產生一個數字,將其作為數組的下標。這個數字就是散列碼,由定義在Object中的、且可能由你的類覆蓋的hashCode()
方法(在電腦科學的術語中稱為散列函數)產生。
為解決數組容量固定的問題,不同的鍵可以產生相同的下標。也就是說,可能會有衝突,即散列碼不必是獨一無二的。因此,數組多大就不重要了,任何鍵總能在數組中找到它的位置。
4.2 理解散列
綜上,散列就是將一個對象產生一個數字儲存下來(作為數組的下標),然後在尋找這個對象時直接找到這個數字就可以了,所以散列的目的是為了提高尋找速度,而手段是將一個對象產生的數字與其關聯並儲存下來(通過數組,稱為散列表)。這個產生的數字就是散列碼。而產生這個散列碼的方法稱為散列函數(hashCode()
)。
4.3 HashMap查詢過程(快速原因)
因此,HashMap
中查詢一個key
的過程就是:
因此,不是查詢整個list
,而是快速地跳到數組的某個位置,只對很少的元素進行比較。這便是HashMap
會如此快速的原因。
4.4 簡單散列Map的實現
散列表中的槽位(slot)通常稱為桶位(bucket)
為使散列均勻,桶的數量通常使用質數(JDK5中是質數,JDK7中已經是2的整數次方了)。
事實證明,質數實際上並不是散列桶的理想容量。近來,(通過廣泛的測試)Java的散列函數都使用2的整數次方。對現代處理器來說,除法與求餘數是最慢的操作。使用2的整數次方長度的散列表,可用
掩碼代替除法。因為get()是使用最多的操作,求餘數的%操作是其開銷最大的部分,而使用2的整數次方可以消除此開銷(也可能對hashCode()有些影響)。
get()方法按照與put()方法相同的方式計算在buckets數組中的索引,這很重要,因為這樣可以保證兩個方法可以計算出相同的位置。
package net.mrliuli.containers;import java.util.*;public class SimpleHashMap<K, V> extends AbstractMap<K, V> { // Choose a prime number for the hash table size, to achieve a uniform distribution: static final int SIZE = 997; // You can't have a physical array of generics, but you can upcast to one: @SuppressWarnings("unchecked") LinkedList<MapEntry<K,V>>[] buckets = new LinkedList[SIZE]; @Override public V put(K key, V value){ int index = Math.abs(key.hashCode()) % SIZE; if(buckets[index] == null){ buckets[index] = new LinkedList<MapEntry<K,V>>(); } LinkedList<MapEntry<K,V>> bucket = buckets[index]; MapEntry<K,V> pair = new MapEntry<K,V>(key, value); boolean found = false; V oldValue = null; ListIterator<MapEntry<K,V>> it = bucket.listIterator(); while(it.hasNext()){ MapEntry<K,V> iPair = it.next(); if(iPair.equals(key)){ oldValue = iPair.getValue(); it.set(pair); // Replace old with new found = true; break; } } if(!found){ buckets[index].add(pair); } return oldValue; } @Override public V get(Object key){ int index = Math.abs(key.hashCode()) % SIZE; if(buckets[index] == null) return null; for(MapEntry<K,V> iPair : buckets[index]){ if(iPair.getKey().equals(key)){ return iPair.getValue(); } } return null; } @Override public Set<Map.Entry<K,V>> entrySet(){ Set<Map.Entry<K,V>> set = new HashSet<Map.Entry<K, V>>(); for(LinkedList<MapEntry<K,V>> bucket : buckets){ if(bucket == null) continue; for(MapEntry<K,V> mpair : bucket){ set.add(mpair); } } return set; } public static void main(String[] args){ SimpleHashMap<String, String> m = new SimpleHashMap<String, String>(); for(String s : "to be or not to be is a question".split(" ")){ m.put(s, s); System.out.println(m); } System.out.println(m); System.out.println(m.get("be")); System.out.println(m.entrySet()); }}
4.5 覆蓋hashCode()
設計`hashCode()`時要考慮的因素:
最重要的因素:無論何時,對同一相對象調用hashCode()都應該產生同樣的值。
此外,不應該使hashCode()依賴於具有唯一性的對象資訊,尤其是使用this的值,這隻能產生很糟糕的hashCode()。因為這樣做無法產生一個新的鍵,使之與put()中原始的索引值對中的鍵相同。即應該使用對象內有意義的識別資訊。也就是說,它必須基於對象的內容產生散列碼。
但是,通過hashCode() equals()必須能夠完全確定對象的身份。
因為在產生桶的下標前,hashCode()還需要進一步處理,所以散列碼的產生範圍並不重要,只要是int即可。
好的hashCode()應該產生分布均勻的散列碼。
《Effective Java Programming Language Guide (Addison-Wesley, 2001)》為怎樣寫出一個像樣的hashCode()給出了一個基本的指導:
給int
變數result
賦予一個非零值常量,如17
為對象內每個有意義的域f
(即每個可以做equals()
操作的域)計算出一個int
散列碼c
:
域類型 |
計算 |
boolean |
c=(f?0:1) |
byte、char、short或int |
c=(int)f |
long |
c=(int)(f^(f>>>32)) |
float |
c=Float.floatToIntBits(f); |
double |
long l = Double.doubleToLongBits(f); |
Object,其equals()調用這個域的equals() |
c=f.hashCode() |
數組 |
對每個元素應用上述規則 |
3. 合并計算散列碼:result = 37 * result + c;
4. 返回result。
5. 檢查hashCode()
最後產生的結果,確保相同的對象有相同的散列碼。
5 選擇不同介面的實現
5.1 微基準測試的危險(Microbenchmarking dangers)
已證明0.0
是包含在Math.random()
的輸出中的,按照數學術語,即其範圍是[0,1)
。
5.2 HashMap的效能因子
HashMap中的一些術語:
容量(Capacity):表中的桶位元(The number of buckets in the table)。
初始容量(Initial capacity):表在建立時所擁有的桶位元。HashMap
和HashSet
都具有允許你指定初始容量的構造器。
尺寸(Size):表中當前儲存的項數。
負載因子(Loadfactor):尺寸/容量。空表的負載因子是0
,而半滿表的負載因子是0.5
,依此類推。負載輕的表產生衝突的可能性小,因此對於插入和尋找都是最理想的(但是會減慢使用迭代器進行遍曆的過程)。HashMap
和HashSet
都具有允許你指定負載因子的構造器,表示當負載情況達到該負載的水平時,容器將自動增加其容量(桶位元),實現方式是使容量大致加倍,並重新將現有對象分布到新的桶位集中(這被稱為再散列)。
HashMap
使用的預設負載因子是0.75
(只有當表達到四分之三滿時,才進行再散列),這個因子在時間和空間代價之間達到了平衡。更高的負載因子可以降低表所需的空間,但會增加尋找代價,這很重要,因為尋找是我們在大多數時間裡所做的操作(包括get()
和put()
)。
6 Collection或Map的同步控制
Collections類有辦法能夠自動同步整個容器。其文法與“不可修改的”方法相似:
package net.mrliuli.containers;import java.util.*;public class Synchronization { public static void main(String[] args){ Collection<String> c = Collections.synchronizedCollection(new ArrayList<String>()); List<String> list = Collections.synchronizedList(new ArrayList<String>()); Set<String> s = Collections.synchronizedSet(new HashSet<String>()); Set<String> ss = Collections.synchronizedSortedSet(new TreeSet<String>()); Map<String, String> m = Collections.synchronizedMap(new HashMap<String, String>()); Map<String, String> sm = Collections.synchronizedSortedMap(new TreeMap<String, String>()); }}
6.1 快速報錯(fail-fast)
Java容器有一種保護機制能夠防止多個進行同時修改同一個容器的內容。Java容器類類庫採用快速報錯(fail-fast)機制。它會探查容器上的任何除了你的進程所進行的操作以外的所有變化,一旦它發現其他進程修改了容器,就會立刻拋出ConcurrentModificationException
異常。這就是“快速報錯”的意思——即,不是使用複雜的演算法在事後來檢查問題。
package net.mrliuli.containers;import java.util.*;public class FailFast { public static void main(String[] args){ Collection<String> c = new ArrayList<>(); Iterator<String> it = c.iterator(); c.add("An Object"); try{ String s = it.next(); }catch(ConcurrentModificationException e){ System.out.println(e); } }}
相關文章:
Java編程思想學習課時(三)第15章-泛型
Java編程思想學習課時(五)第18章-Java IO系統