在本章中,我定義了一個比
MyLinearMap
更好的Map
接口實現,MyBetterMap
,並引入哈希,這使得MyBetterMap
效率更高。
1.哈希
爲了提高MyLinearMap
的性能,我們將編寫一個新的類,它被稱爲MyBetterMap
,它包含MyLinearMap
對象的集合。它在內嵌的映射之間劃分鍵,因此每個映射中的條目數量更小,這加快了findEntry
,以及依賴於它的方法的速度。
類定義的開始:
public class MyBetterMap<K, V> implements Map<K, V> {
protected List<MyLinearMap<K, V>> maps;
public MyBetterMap(int k) {
makeMaps(k);
}
// k值關係到hashCode的分配
protected void makeMaps(int k) {
maps = new ArrayList<MyLinearMap<K, V>>(k);
for (int i=0; i<k; i++) {
maps.add(new MyLinearMap<K, V>());
}
}
}
實例變量maps
是一組MyLinearMap
對象。構造函數接受一個參數k
,決定至少最開始,要使用多少個映射。然後makeMaps
創建內嵌的映射並將其存儲在一個ArrayList
中。
現在,完成這項工作的關鍵是,我們需要一些方法來查看一個鍵,並決定應該進入哪個映射(首先要放進去,放到哪裏用HashCode算法決定)。當我們put
一個新的鍵時,我們選擇一個映射;當我們get
同樣的鍵時,我們必須記住我們把它放在哪裏(重點是要確定如何跟蹤)。
一種可能性是隨機選擇一個子映射,並跟蹤我們把每個鍵放在哪裏(無法跟蹤)。
一個更好的方法是使用一個哈希函數,它接受一個Object
,一個任意的Object
,並返回一個稱爲哈希碼的整數。重要的是,如果它不止一次看到相同的Object
,它總是返回相同的哈希碼。這樣,如果我們使用哈希碼來存儲鍵,當我們查找時,我們將得到相同的哈希碼。
在Java中,每個Object
都提供了hashCode
,一種計算哈希函數的方法。這種方法的實現對於不同的對象是不同的;我們會很快看到一個例子。
選擇到正確的子映射:
/**
* @Author Ragty
* @Description 爲一個給定的鍵選擇正確的子映射(同makeMaps的初始化數量有關)
* @Date 10:53 2019/4/23
**/
protected MyLinearMap<K, V> chooseMap(Object key) {
int index = 0;
if (key != null) {
index = Math.abs(key.hashCode()) % maps.size();
}
return maps.get(index);
}
如果key
是null
,我們選擇索引爲0
的子映射。否則,我們使用hashCode
獲取一個整數,調用Math.abs
來確保它是非負數,然後使用餘數運算符%
,這保證結果在0
和maps.size()-1
之間。所以index
總是一個有效的maps
索引。然後chooseMap
返回爲其所選的映射的引用。
我們使用chooseMap
的put
和get
,所以當我們查詢鍵的時候,我們得到添加時所選的相同映射,我們選擇了相同的映射。至少應該是 - 稍後我會解釋爲什麼這可能不起作用。
put
和get
實現:
public V put(K key, V value) {
MyLinearMap<K, V> map = chooseMap(key);
return map.put(key, value);
}
public V get(Object key) {
MyLinearMap<K, V> map = chooseMap(key);
return map.get(key);
}
我們使用chooseMap
來找到正確的子映射,然後在子映射上調用一個方法,我們考慮一下性能.
如果在k
個子映射中分配了n
個條目,則平均每個映射將有n/k
個條目。當我們查找一個鍵時,我們必須計算其哈希碼,這需要一些時間,然後我們搜索相應的子映射。
因爲MyBetterMap
中的條目列表,比MyLinearMap
中的短k
倍,我們的預期是ķ
倍的搜索速度。但運行時間仍然與n
成正比(n/k),所以MyBetterMap
仍然是線性的。在下一個練習中,你將看到如何解決這個問題。
關於此問題的思考,現在考慮一種比較極端的情況,如果恰好所有數據都集中在一個子映射中,那麼效率將跟線性方法一樣.
2.哈希如何工作
哈希函數的基本要求是,每次相同的對象應該產生相同的哈希碼。對於不變的對象,這是比較容易的。對於具有可變狀態的對象,我們必須花費更多精力。
作爲一個不可變對象的例子,我將定義一個SillyString
類,它包含一個String
:
public class SillyString {
private final String innerString;
public SillyString(String innerString) {
this.innerString = innerString;
}
public String toString() {
return innerString;
}
使用它來展示,一個類如何定義它自己的哈希函數:
@Override
public boolean equals(Object other) {
return this.toString().equals(other.toString());
}
@Override
public int hashCode() {
int total = 0;
for (int i=0; i<innerString.length(); i++) {
total += innerString.charAt(i);
}
return total;
}
注意SillyString
重寫了equals
和hashCode
。這個很重要。爲了正常工作,equals
必須和hashCode
一致,這意味着如果兩個對象被認爲是相等的 - 也就是說,equals
返回true
- 它們應該有相同的哈希碼。但這個要求只是單向的;如果兩個對象具有相同的哈希碼,則它們不一定必須相等。(不可逆)
hashCode
的原理是,迭代String
中的字符並將它們相加。當你向int
添加一個字符時,Java 將使用其 Unicode 代碼點,將字符轉換爲整數。
該哈希函數滿足要求:如果兩個SillyString
對象包含相等的內嵌字符串,則它們將獲得相同的哈希碼。
這可以正常工作,但它可能不會產生良好的性能,因爲它爲許多不同的字符串返回相同的哈希碼。如果兩個字符串以任何順序包含相同的字母,它們將具有相同的哈希碼。即使它們不包含相同的字母,它們可能會產生相同的總量,例如"ac"
和"bb"
。或者abc
同cab
的哈希碼也是一致的
如果許多對象具有相同的哈希碼,它們將在同一個子映射中。如果一些子映射比其他映射有更多的條目,那麼當我們有k
個映射時,加速比可能遠遠小於k
。所以哈希函數的目的之一是統一;也就是說,以相等的可能性,在這個範圍內產生任何值。(等概率的在子映射中分配條目,這正是之前我們所擔心的)
3.哈希和可變性
String
是不可變的,SillyString
也是不可變的,因爲innerString
定義爲final
。一旦你創建了一個SillyString
,你不能使innerString
引用不同的String
,你不能修改所指向的String
。因此,它將始終具有相同的哈希碼。
我們現在用一個Array,和以上的一樣,改變的是之前是String,現在是數組(可變)
public class SillyArray {
private final char[] array;
public SillyArray(char[] array) {
this.array = array;
}
public String toString() {
return Arrays.toString(array);
}
@Override
public boolean equals(Object other) {
return this.toString().equals(other.toString());
}
@Override
public int hashCode() {
int total = 0;
for (int i=0; i<array.length; i++) {
total += array[i];
}
System.out.println(total);
return total;
}
SillyArray
也提供setChar
,它能夠修改修改數組內的字符。
public void setChar(int i, char c) {
this.array[i] = c;
}
現在假設我們創建了一個SillyArray
,並將其添加到map
。
SillyArray array1 = new SillyArray("Word1".toCharArray());
map.put(array1, 1);
這個數組的哈希碼是461
。現在如果我們修改了數組內容,之後嘗試查詢它,像這樣:
array1.setChar(0, 'C');
Integer value = map.get(array1);
修改之後的哈希碼是441
。使用不同的哈希碼,我們就很可能進入了錯誤的子映射。這就很糟糕了。
一般來說,使用可變對象作爲散列數據結構中的鍵是很危險的,這包括MyBetterMap
和HashMap
。如果你可以保證映射中的鍵不被修改,或者任何更改都不會影響哈希碼,那麼這可能是正確的。但是避免這樣做可能是一個好主意。(個人認爲還是直接避免這種不安全的操作比較好)