一個關於自定義類型作為HashMap的key的問題


在之前的項目需要用到以自定義類型作為HashMap的key,遇到一個問題:如果修改了已經存儲在HashMap中的實例,會發生什么情況呢?用一段代碼來試驗:

import java.util.HashMap;
import java.util.Map;

public class TestHashMap {

    public static void main(String[] args) {
        testObjAsKey();
    }

    private static void testObjAsKey() {
        class Person {
            public String familyName;
            public String givenName;

            public Person(String familyName, String givenName) {
                this.familyName = familyName;
                this.givenName = givenName;
            }

            @Override
            public int hashCode() {
                final int prime = 31;
                int result = 1;
                result = prime * result
                        + ((familyName == null) ? 0 : familyName.hashCode());
                result = prime * result
                        + ((givenName == null) ? 0 : givenName.hashCode());
                return result;
            }

            @Override
            public boolean equals(Object obj) {
                if (this == obj) {
                    return true;
                }
                if (obj == null) {
                    return false;
                }
                if (!(obj instanceof Person)) {
                    return false;
                }
                Person other = (Person) obj;
                if (familyName == null) {
                    if (other.familyName != null) {
                        return false;
                    }
                } else if (!familyName.equals(other.familyName)) {
                    return false;
                }
                if (givenName == null) {
                    if (other.givenName != null) {
                        return false;
                    }
                } else if (!givenName.equals(other.givenName)) {
                    return false;
                }
                return true;
            }

            @Override
            public String toString() {
                return "Person(" + familyName + ", " + givenName + ")";
            }

        }

        Map<Person, Integer> map = new HashMap<Person, Integer>();
        Person person1 = new Person("zhang", "san");

        map.put(person1, 1);
        System.out.println("Value of " + person1 + " is " + map.get(person1));

        person1.givenName = "si";
        System.out.println("'zhang san' is changed to 'zhang si'");
        System.out.println("Value of 'zhang san' is " + map.get(new Person("zhang", "san")));
        System.out.println("Value of 'zhang si' is " + map.get(new Person("zhang", "si")));
        System.out.println("Value of `person1` is " + map.get(person1));

    }

}

程序的輸出是什么?答案見下

Value of Person(zhang, san) is 1
'zhang san' is changed to 'zhang si'
Value of 'zhang san' is null
Value of 'zhang si' is null
Value of `person1` is null

為什么這樣呢?這要從HashMap的實現進行分析。HashMap使用一個Entry數組保存內部的元素(Entry是用來保存<key, value="">對的類型)。數組的每個slot保存一個鏈表的頭指針,這個鏈表內的元素都是hashCode相同的entry

Entry[] tables
    +---+
    | 0 | -> entry_0_0 -> entry_0_1 -> null
    +---+
    | 1 | -> null
    +---+
    |   |

     ...

    |n-1| -> entry_n-1_0 -> null
    +---+

HashMapput()get()方法基本原理如下: - put一個元素的時候,根據key的hashCode()方法計算出hash值,進而算出相應的數組下標,然后將這個新的entry加入到鏈表中。 - get一個key的時候,根據key的hash值找到相應的數組下標,然后遍歷這個鏈表,並查找和當前key相等的entry。判斷兩個元素是否相等時使用使用equals()方法

上面的例子中,在put操作之后,map內部的存儲是這樣的(假設這個元素被存儲在了第i個slot):

    |   | -> null
    +---+
    | i | -> entry<person1("zhang", "san"), 1> -> null
    +---+
    |   | -> null

注意,當我們修改了person1的時候,並沒有修改它存儲在map中的位置。也就是說,修改之后的map的內部存儲是這樣的:

    |   | -> null
    +---+
    | i | -> entry<person1("zhang", "si"), 1> -> null
    +---+
    |   | -> null

person1仍然存儲在第i個slot里。

get(new Person("zhang", "si"))的時候,HashMap將先根據hash值計算它應該位於第幾個slot,比如是j。由於根據Person("zhang", "san")計算出來的下標是i,i和j很可能是不相同的,那么第j個slot是空的(因為之前只put了一個元素且位於第i個slot),因此get()方法將返回null

get(new Person("zhang", "san"))的時候,HashMap將計算它應該位於第i個slot,然后在這個鏈表中查找。這個鏈表中存儲了一個元素,但是當前的key是("zhang", "si"),使用equals()方法判斷這兩個key是否相等時將返回false,也就是說,HashMap在第i個slot所維護的鏈表中沒有找到和當前key相等的元素,因此get()方法將返回null

get(person1)的時候,因為person1現在的值是("zhang", "si"),所以和get(new Person("zhang", "si"))的情況是完全一樣的。

總結一下,上面的分析說明,如果不小心改變了已經存儲在HashMap中的key值,那么將引起潛在的錯誤。顯然,避免這個問題比較好的方法是,如果打算將自己定義的一種數據類型作為key,那么將這個類型設計成不可變的(immutable)。比如IntegerString等都是不可變的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM