綜上,HashMap<K,V>就是一個用來存儲<鍵、值>數據對的機制,其中鍵key「映射」到保存值(value)的存儲地址,映射過程使用了哈希函數。也就是鍵(key)通過哈希函數運算後能夠獲得值(value)的地址。
對上面電話號碼簿的例子,電話號碼簿體現爲HashMap<K,V>的一個實例,鍵key爲手機號,值(value)也爲手機號。鍵(手機號)通過哈希函數運算(取手機號後4位)後能夠獲得值(手機號)的地址。
這個問題與前2個問題的區別是,要查詢的數據不是單個數字,這就很難利用前2個示例中的方法構建一個易於查詢的花名冊。可是能夠試想,假如能夠經過某種運算將名字變成一個0到10000之間的一個數字,並且名字不一樣時,產生的數字不一樣,那麼就能夠利用上述的方法構建一個易於查詢的花名冊。
4 哈希函數(Hash Function)的定義
上例中某種運算(將名字變成一個0到10000之間的一個數字)就能夠被稱做是哈希函數。
哈希函數更專業的定義是:哈希函數是任意一種算法,它能夠將任意長度的原數據映射爲固定長度的結果數據。
由於哈希函數一般將可變長度的原數據,「切碎(hash)」成固定長度數據,對各部分處理後造成一個固定長度的數據,因此被形象的稱爲哈希函數。
號碼簿問題中,取電話號碼中的後4位這個運算,就是將一個長數據映射爲了一個短數據,因此也能夠稱爲哈希函數。
因爲產生的數據長度固定,因此結果數據就能夠用來做爲數組的索引值,在相應位置保存原數據,就能夠加快查詢。
- 從十進制角度看,若是產生的數據在0-10000之間,也就是4位十進制數時,就能夠建立一個10000個數據的數組,用哈希函數的結果作爲索引值。
- 從二進制角度看,若是產生的數據在0-0x7F之間,也就是8位二進制數時,就能夠建立一個128個數據的數組,用哈希函數的結果作爲索引值。
5 如何設計合適的哈希函數
能夠想象爲了減小衝突,加快查詢,不一樣原數據通過哈希運算後產生的數值應該最大可能的不一樣。因此一個優秀的哈希函數必然具備這樣的性質。
注意:如下內容的敘述從數學理論的角度並不徹底嚴密與準確,且缺乏證實。更嚴謹的學習應該查看相關著做或者參加專門課程。
質數與求模運算正好具備這樣的性質:
假若有一個質數Z,其遠大於數S,那麼對於運算:
( n * Z ) % S
其中n表明從1到無窮的任意整數,*爲乘法運算,%爲求模運算
對應任意n,運算的結果均勻的分佈在0到S之間。
好比對於質數211和數8:
(1*211) % 8 = 3 (67*211) % 8 = 1
(2*211) % 8 = 6 (68*211) % 8 = 4
(3*211) % 8 = 1 (69*211) % 8 = 7
(4*211) % 8 = 4 (70*211) % 8 = 2
(5*211) % 8 = 7 (71*211) % 8 = 5
(6*211) % 8 = 2 (72*211) % 8 = 0
(7*211) % 8 = 5 (73*211) % 8 = 3
(8*211) % 8 = 0 (74*211) % 8 = 6
因此對於上面花名冊的例子,若是能夠將名字通過哈希運算獲得0到10000之間的數值,就能夠實現快速查詢。因爲字符在電腦中一般用Unicode代碼表示,查出名字的Unicode代碼,「張」的Unicode十進制代碼爲24352,「三」的Unicode十進制代碼爲19977,選取質數9656717,進行如下運算:((24352 + 19977) * 9656717) % 10000 = 5168。這樣就獲得了0到10000之間的數值,參照以前的例子,就能夠構造一個數組來加快查詢。
Unicode代碼查詢網址:
http://www.unicode.org/charts/unihan.html
質數表,Table of Primes from 1 to 1 000 000 000 000:
http://www.walter-fendt.de/m14e/primes.htm
5.1 java.lang.String類中字符串的哈希函數
在Oracle公司的Java API實現中,String類的hashcode()函數計算了字符串的哈希值,源代碼以下。從註釋和程序中能夠看出,計算公式爲hashall = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],是將字符串各個字符的UTF-16代碼乘以31的ni次方後相加獲得的。31爲質數,31^(ni)雖然不是質數,可是性質接近質數。但並無發現顯式的求模運算%,這是由int類型數據算術運算後獲得的,若是值超過了int類型的最大值時,高位被自動拋棄,這就至關於對2147483648(十六進制0x7FFF)求模,因此結果在0到2147483648之間。
/**
* Returns a hash code for this string. The hash code for a
* <code>String</code> object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using <code>int</code> arithmetic, where <code>s[i]</code> is the
* <i>i</i>th character of the string, <code>n</code> is the length of
* the string, and <code>^</code> indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
6 Java API中的HashMap類實現簡介
6.1 HashMap類中哈希值的計算方法
經過查看其源代碼,能夠看出HashMap類中哈希值的計算方法。
類中的哈希值是經過final int hash(Object k)函數實現的,首先根據鍵(key)對象的hashcode函數計算鍵對象的hash值:k.hashCode(),而後內部再進行相應的移位和求異或運算,獲得內部使用的hash值。能夠看出hash值由int類型表示,則其值在0到Interger.MAX_VALUE之間。但實際內部存儲用的數組長度由HashMap的容量決定,因此根據hash值獲得對象在數組中的索引值,還須要近一步計算,下段中進行了說明。
/**
* Retrieve object hash code and applies a supplemental hash function to the
* result hash, which defends against poor quality hash functions. This is
* critical because HashMap uses power-of-two length hash tables, that
* otherwise encounter collisions for hashCodes that do not differ
* in lower bits. Note: Null keys always map to hash 0, thus index 0.
*/
final int hash(Object k) {
int h = 0;
if (useAltHashing) {//因爲沒看完整的源代碼,此處目的沒看明白,根據字面理解多是其它基於此類的之類,若是不滿意默認的哈希函數算法,可使用此算法代替。
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
h ^= k.hashCode();//計算鍵對象的hash值,以後與0求異或運算
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4); //移位異或運算等,使hash值更分散下降衝突可能
}
6.2 根據鍵(key)對象查詢值(value)對象
根據鍵對象查詢<K,V>對象的方法,涉及到的源代碼以下。
首先public V get(Object key)函數中,調用getEntry(key)函數,由鍵對象得到相應值的Entry<K,V>的地址entry。
從Entry<K,V>源代碼(這裏沒有粘貼過來)能夠看出,Entry<K,V>是類中定義的新類,繼承至Map.Entry<K,V>。該對象中保存了鍵(key)對象和相應的值(value)對象,幷包含有指向下個Entry<K,V>地址的變量,這樣能夠實現鏈表功能,用於解決衝突。若是衝突產生時(不一樣鍵對象的hash值相同),將hash值相同的對象其依次放在此鏈表中。
getEntry(key)函數中首先由hash(key)計算鍵對象的hash值。
而後由indexFor(hash, table.length)函數根據hash值得到Entry<K,V>[]數組的索引值,該函數中h & (length-1)運算將hash值由原來的0到Interger.MAX_VALUE之間映射到0到(length-1)之間,這樣就能夠看成該數組的索引值。
而後Entry<K,V> e = table[indexFor(hash, table.length)]根據索引值,將須要的數據找到。
table是Entry<K,V>[]類型的數組,其中保存了指向相應Entry<K,V>的地址。
for程序段中,若是有衝突,則依次遍歷此鏈表,找到與指定鍵對象對應的值對象。將Entry<K,V>對象返回get(Object key)函數。
最後get(Object key)函數調用entry.getValue()得到相應的值對象。
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*/
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
6.3 HashMap類的容量
從以前的例子中,能夠知道查詢速度的改進是因爲用空間換取了時間,因此HashMap類的容量越大,效率越高,可是空間佔用約多。
通過權衡,類中定義了填充率(loadFactor),默認爲0.75;容量(capacity),默認值爲16。源代碼以下:
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
類始終保持類中保存的數據量小於門限(threshold) = 容量(capacity)* 填充率(loadFactor)。每次添加的新的數據時,都檢測數據量(size)是否超過門限(threshold)。若是超限則調用resize(2 * table.length)函數,將類的容量增大。源代碼以下:
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
6.4 調整HashMap類的容量對性能的影響
調整HashMap類的容量的函數resize(int newCapacity)源代碼以下,從新調整大小,須要新建一個Entry[]數組,而後調用transfer(newTable, rehash)函數將以前數組中的值調整到新數組中。
transfer(newTable, rehash)函數中調用hash(e.key)函數從新計算了鍵對象的哈希值,根據哈希值將舊Entry[]數組中數據放到新Entry[]數組中。
因此調整HashMap類的容量形成了如下影響:
- 新建一個Entry[]數組,須要格外的空間
- 從新計算了鍵對象的哈希值,須要格外的運行時間
- 因爲Entry[]數組長度變化,各元素在HashMap中的內部位置發生了改變
綜上,要根據時間狀況,設計HashMap類的容量和填充率,盡少調整容量的次數。
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
6.5 最後一個例子,電話簿PhoneBook
以前示例中的電話簿中沒有人名,這裏添加人名。PhoneBook 擴展了HashMap類。這樣能夠直接使用其函數。電話簿中的每條內容由<String 人名, String 號碼>組成。將人名做爲鍵,號碼做爲值,因此能夠根據人名得到他/她的電話號碼。
因爲使用了字符串做爲鍵,因此能夠利用其已經實現的hashcode()函數實現hash值的計算。
因爲HashMap要求鍵值各不相同,因此此電話簿,不能有重名,還須要進一步改進。
import java.util.HashMap;
// PhoneBook 擴展了HashMap類。這樣能夠直接使用其函數。
// 電話簿中的每條內容由<String 人名, String 號碼>組成。
// 將人名做爲鍵,號碼做爲值,因此能夠根據人名得到他/她的電話號碼
public class PhoneBook extends HashMap<String,String> {
PhoneBook(){
super();
}
//測試
public static void main(String[] args) {
PhoneBook pb = new PhoneBook();
String[][] intial = new String[][]{
{ "張三","286 3545 1285" },
{ "李四","250 4592 8502" },
{ "王五","239 2085 1032" },
{ "趙六","230 1932 0543" },
{ "王二麻子","259 1937 1408" },
{ "段譽","251 8592 1459" },
{ "王語嫣","252 2309 7934" },
{ "虛竹","249 2942 9285" },
{ "夢姑","289 0103 8482" },
{ "喬峯","279 0094 1342" }
};
//將電話保存在電話簿中
for(int i = 0; i < intial.length; i++) {
pb.put(intial[i][0], intial[i][1]);
}
//測試
System.out.println("電話簿中共保存了" + pb.size() + "個電話號碼。" );
String name = new String("喬峯");
Boolean bl = pb.containsKey(name);//查詢是否包含該人名
System.out.println("電話簿中" + ( bl ? "查到" : "未查到" ) + name
+ "的電話號碼。"
+ ( bl ? ("電話號碼是" + pb.get(name) + "。") : ""));
//測試
name = new String("王語嫣");
bl = pb.containsKey(name);
System.out.println("電話簿中" + ( bl ? "查到" : "未查到" ) + name
+ "的電話號碼。"
+ ( bl ? ("電話號碼是" + pb.get(name) + "。") : ""));
//測試
name = new String("星秀老仙");
bl = pb.containsKey(name);
System.out.println("電話簿中" + ( bl ? "查到" : "未查到" ) + name
+ "的電話號碼。"
+ ( bl ? ("電話號碼是" + pb.get(name) + "。") : ""));
}
運行程序後,根據輸出能夠看出電話簿正常工做:
電話簿中共保存了10個電話號碼。
電話簿中查到喬峯的電話號碼。電話號碼是279 0094 1342。
電話簿中查到王語嫣的電話號碼。電話號碼是252 2309 7934。
電話簿中未查到星秀老仙的電話號碼。
7 參考資料
[1] Hash function http://en.wikipedia.org/wiki/Hash_function [2] 麻省理工學院公開課:算法導論> 哈希表 http://v.163.com/movie/2010/12/R/E/M6UTT5U0I_M6V2TG4RE.html [3] Java官方API(Oracle Java SE7)源代碼,下載安裝JDK後,源代碼位於安裝根目錄的src.zip文件中 http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html [4] OpenJDK源代碼下載(包括了HotSpot虛擬機、各個系統下API的源代碼,其中API源代碼位於openjdk\jdk\src\share\classes文件夾下): https://jdk7.java.net/source.html