![](/img/trans.png)
[英]Can different objects with same value for the attributes have same hashcode in Java
[英]Why can hashCode() return the same value for different objects in Java?
我正在閱讀的《 Head First Java 》一書中的一句話:
關鍵是哈希碼可以相同而不必保證對象相等,因為
hashCode()
方法中使用的“哈希算法”可能恰好為多個對象返回相同的值。
為什么hashCode()
方法可能會為不同的對象返回相同的值? 這不會引起問題嗎?
散列一個對象意味着“找到一個好的、描述性的值(數字),可以由同一個實例一次又一次地復制”。 因為來自 Java 的Object.hashCode()
的哈希碼是int
類型,所以您只能有2^32
不同的值。 這就是為什么當兩個不同的對象產生相同的 hashCode 時,取決於散列算法,您會遇到所謂的“沖突”。
通常,這不會產生任何問題,因為hashCode()
主要與equals()
一起使用。 例如, HashMap
將對其鍵調用hashCode()
,以了解鍵是否可能已包含在 HashMap 中。 如果 HashMap 沒有找到哈希碼,很明顯 HashMap 中還沒有包含該鍵。 但如果是這樣,它將必須使用equals()
仔細檢查所有具有相同哈希碼的鍵。
IE
A.hashCode() == B.hashCode() // does not necessarily mean
A.equals(B)
但
A.equals(B) // means
A.hashCode() == B.hashCode()
如果equals()
和hashCode()
實現正確。
有關一般hashCode
協定的更准確描述,請參閱Javadoc 。
只有超過 40 億個可能的哈希碼( int
的范圍),但您可以選擇創建的對象數量要大得多。 因此,根據鴿巢原則,一些對象必須共享相同的哈希碼。
例如,包含來自 AZ 的 10 個字母的可能字符串的數量是 26**10,即 141167095653376。不可能為所有這些字符串分配一個唯一的哈希碼。 這也不重要——散列碼不需要是唯一的。 它只需要對真實數據沒有太多的沖突。
哈希表的想法是您希望能夠以高效的方式實現稱為字典的數據結構。 字典是鍵/值存儲,即,您希望能夠將某些對象存儲在某個鍵下,稍后能夠使用相同的鍵再次檢索它們。
訪問值的最有效方法之一是將它們存儲在數組中。 例如,我們可以實現一個字典,它使用整數作為鍵,使用字符串作為值,如下所示:
String[] dictionary = new String[DICT_SIZE];
dictionary[15] = "Hello";
dictionary[121] = "world";
System.out.println(dictionary[15]); // prints "Hello"
不幸的是,這種方法根本不是很通用:數組的索引必須是一個整數值,但理想情況下我們希望能夠為我們的鍵使用任意種類的對象,而不僅僅是整數。
現在,解決這一點的方法是找到一種將任意對象映射到整數值的方法,然后我們可以將其用作數組的鍵。 在 Java 中,這就是hashCode()
的作用。 所以現在,我們可以嘗試實現一個 String->String 字典:
String[] dictionary = new String[DICT_SIZE];
// "a" -> "Hello"
dictionary["a".hashCode()] = "Hello";
// "b" -> "world"
dictionary["b".hashCode()] = "world";
System.out.println(dictionary["b".hashCode()]); // prints world
但是,嘿,如果有一些我們想用作鍵的對象,但它的hashCode
方法返回一個大於或等於DICT_SIZE
的值怎么辦? 然后我們會得到一個 ArrayIndexOutOfBoundsException,這是不可取的。 所以,讓我們把它做得盡可能大,對吧?
public static final int DICT_SIZE = Integer.MAX_VALUE // Ooops!
但這意味着我們必須為我們的數組分配大量內存,即使我們只打算存儲一些項目。 所以這不是最好的解決方案,事實上我們可以做得更好。 假設我們有一個函數h
,對於任何給定的DICT_SIZE
,將任意整數映射到[0, DICT_SIZE[
范圍內。 然后我們可以將h
應用於鍵對象的hashCode()
方法返回的任何內容,並確保我們停留在底層數組的邊界內。
public static int h(int value, int DICT_SIZE) {
// returns an integer >= 0 and < DICT_SIZE for every value.
}
該函數稱為哈希函數。 現在我們可以調整我們的字典實現來避免 ArrayIndexOutOfBoundsException:
// "a" -> "Hello"
dictionary[h("a".hashCode(), DICT_SIZE)] = "Hello"
// "b" -> "world"
dictionary[h("b".hashCode(), DICT_SIZE)] = "world"
但這引入了另一個問題:如果h
將兩個不同的鍵索引映射到相同的值怎么辦? 例如:
int keyA = h("a".hashCode(), DICT_SIZE);
int keyB = h("b".hashCode(), DICT_SIZE);
可能會為keyA
和keyB
產生相同的值,在這種情況下,我們會不小心覆蓋數組中的值:
// "a" -> "Hello"
dictionary[keyA] = "Hello";
// "b" -> "world"
dictionary[keyB] = "world"; // DAMN! This overwrites "Hello"!!
System.out.println(dictionary[keyA]); // prints "world"
好吧,你可能會說,那么我們只需要確保我們以永遠不會發生這種情況的方式實現h
。 不幸的是,這通常是不可能的。 考慮以下代碼:
for (int i = 0; i <= DICT_SIZE; i++) {
dictionary[h(i, DICT_SIZE)] = "dummy";
}
這個循環在字典中存儲DICT_SIZE + 1
值(實際上總是相同的值,即字符串“dummy”)。 嗯,但是數組只能存儲DICT_SIZE
不同的條目,這意味着,當我們使用h
,我們會(至少)覆蓋一個條目。 或者換句話說, h
會將兩個不同的鍵映射到相同的值:如果 n 只鴿子試圖進入 n-1 個鴿子洞,這些“碰撞”是不可避免的。 他們中至少有兩個必須進入同一個洞。
但我們能做的是擴展我們的實現,讓數組可以在同一個索引下存儲多個值。 這可以通過使用列表輕松完成。 所以不要使用:
String[] dictionary = new String[DICT_SIZE];
我們寫:
List<String>[] dictionary = new List<String>[DICT_SIZE];
(旁注:請注意,Java 不允許創建泛型數組,因此上面的行不會編譯——但您明白了)。
這將改變對字典的訪問,如下所示:
// "a" -> "Hello"
dictionary[h("a".hashCode(), DICT_SIZE)].add("Hello");
// "b" -> "world"
dictionary[h("b".hashCode(), DICT_SIZE)].add("world");
如果我們的哈希函數h
為我們所有的鍵返回不同的值,這將導致每個列表只有一個元素,並且檢索元素非常簡單:
System.out.println(dictionary[h("a".hashCode(), DICT_SIZE)].get(0)); // "Hello"
但是我們已經知道,通常h
有時會將不同的鍵映射到同一個整數。 在這些情況下,列表將包含多個值。 對於檢索,我們必須遍歷整個列表才能找到“正確”的值,但我們如何識別它呢?
好吧,我們可以始終將完整的 (key,value) 對存儲在列表中,而不是單獨存儲值。 然后查找將分兩步執行:
現在添加和檢索變得如此復雜,以至於為這些操作對待我們自己單獨的方法並不算不雅:
List<Pair<String,String>>[] dictionary = List<Pair<String,String>>[DICT_SIZE];
public void put(String key, String value) {
int hashCode = key.hashCode();
int arrayIndex = h(hashCode, DICT_SIZE);
List<Pair<String,String>> listAtIndex = dictionary[arrayIndex];
if (listAtIndex == null) {
listAtIndex = new LinkedList<Pair<Integer,String>>();
dictionary[arrayIndex] = listAtIndex;
}
for (Pair<String,String> previouslyAdded : listAtIndex) {
if (previouslyAdded.getKey().equals(key)) {
// the key is already used in the dictionary,
// so let's simply overwrite the associated value
previouslyAdded.setValue(value);
return;
}
}
listAtIndex.add(new Pair<String,String>(key, value));
}
public String get(String key) {
int hashCode = key.hashCode();
int arrayIndex = h(hashCode, DICT_SIZE);
List<Pair<String,String>> listAtIndex = dictionary[arrayIndex];
if (listAtIndex != null) {
for (Pair<String,String> previouslyAdded : listAtIndex) {
if (previouslyAdded.getKey().equals(key)) {
return previouslyAdded.getValue(); // entry found!
}
}
}
// entry not found
return null;
}
因此,為了使這種方法起作用,我們實際上需要兩個比較操作:用於在數組中查找列表的 hashCode 方法(如果hashCode()
和h
都很快,這會很快工作)和一個equals
方法,我們需要在通過列表。
這是散列的一般思想,你會從java.util.Map.
認出put
和get
方法。 當然,上面的實現是過於簡單化了,但它應該說明了這一切的要點。
當然,這種方法不限於字符串,它適用於所有類型的對象,因為方法hashCode()
和equals
是頂級類 java.lang.Object 的成員,所有其他類都繼承自該類。
如您所見,兩個不同的對象是否在其hashCode()
方法中返回相同的值並不重要:上述方法將始終有效! 但仍然希望它們返回不同的值以降低h
產生的散列沖突的機會。 我們已經看到這些通常無法 100% 避免,但是我們得到的沖突越少,我們的哈希表就變得越高效。 在最壞的情況下,所有鍵都映射到相同的數組索引:在這種情況下,所有鍵值對都存儲在一個列表中,然后查找一個值將成為一個成本與哈希表大小成線性關系的操作。
hashCode() 值可用於快速查找對象,方法是將哈希碼用作存儲它的哈希表存儲桶的地址。
如果多個對象從 hashCode() 返回相同的值,則意味着它們將存儲在同一個桶中。 如果許多對象存儲在同一個桶中,則意味着平均需要更多比較操作才能查找給定對象。
而是使用 equals() 來比較兩個對象以查看它們在語義上是否相等。
據我了解,hashcode 方法的工作是創建用於散列元素的存儲桶,以便更快地檢索。 如果每個對象都將返回相同的值,則無需進行任何散列。
我不得不認為,對於具有相同哈希碼的 2 個對象來說,這是一種非常低效的哈希算法。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.