簡體   English   中英

為什么Java中不同對象的hashCode()可以返回相同的值?

[英]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);

可能會為keyAkeyB產生相同的值,在這種情況下,我們會不小心覆蓋數組中的值:

// "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) 對存儲在列表中,而不是單獨存儲值。 然后查找將分兩步執行:

  1. 應用哈希函數從數組中檢索正確的列表。
  2. 遍歷存儲在檢索列表中的所有對:如果找到具有所需鍵的對,則返回該對的值。

現在添加和檢索變得如此復雜,以至於為這些操作對待我們自己單獨的方法並不算不雅:

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.認出putget方法。 當然,上面的實現是過於簡單化了,但它應該說明了這一切的要點。

當然,這種方法不限於字符串,它適用於所有類型的對象,因為方法hashCode()equals是頂級類 java.lang.Object 的成員,所有其他類都繼承自該類。

如您所見,兩個不同的對象是否在其hashCode()方法中返回相同的值並不重要:上述方法將始終有效! 但仍然希望它們返回不同的值以降低h產生的散列沖突的機會。 我們已經看到這些通常無法 100% 避免,但是我們得到的沖突越少,我們的哈希表就變得越高效。 在最壞的情況下,所有鍵都映射到相同的數組索引:在這種情況下,所有鍵值對都存儲在一個列表中,然后查找一個值將成為一個成本與哈希表大小成線性關系的操作。

hashCode() 值可用於快速查找對象,方法是將哈希碼用作存儲它的哈希表存儲桶的地址。

如果多個對象從 hashCode() 返回相同的值,則意味着它們將存儲在同一個桶中。 如果許多對象存儲在同一個桶中,則意味着平均需要更多比較操作才能查找給定對象。

而是使用 equals() 來比較兩個對象以查看它們在語義上是否相等。

據我了解,hashcode 方法的工作是創建用於散列元素的存儲桶,以便更快地檢索。 如果每個對象都將返回相同的值,則無需進行任何散列。

我不得不認為,對於具有相同哈希碼的 2 個對象來說,這是一種非常低效的哈希算法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM