面试官:我们都知道在使用HashMap时,经常使用String类型或者Integer类型作为key,那我们可以使用自定义的对象吗?

应聘者:可以,能用其他对象,必须是immutable的,但是自实现的类必须Override两个方法:equals()和hashCode()。因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象;

面试官:那为什么String类型天生就可以做为键值呢?

应聘者:因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了;


可变对象

可变对象是指创建后自身状态能改变的对象。换句话说,可变对象是该对象在创建后它的哈希值可能被改变。

让我们看看下面的一段示例代码,在下面的代码中,对象MutableKey的键在创建时变量 i=10 j=20,哈希值是1291。然后我们改变实例的变量值,该对象的键 i 和 j 从10和20分别改变成30和40。现在Key的哈希值已经变成1931。显然,这个对象的键在创建后发生了改变。所以类MutableKey是可变的。

 1public class MutableKey {
2    private int i;
3    private int j;
4
5    public MutableKey(int i, int j) {
6        this.i = i;
7        this.j = j;
8    }
9
10    public final int getI() {
11        return i;
12    }
13
14    public final void setI(int i) {
15        this.i = i;
16    }
17
18    public final int getJ() {
19        return j;
20    }
21
22    public final void setJ(int j) {
23        this.j = j;
24    }
25
26    @Override
27    public int hashCode() {
28        final int prime = 31;
29        int result = 1;
30        result = prime * result + i;
31        result = prime * result + j;
32        return result;
33    }
34
35    @Override
36    public boolean equals(Object obj) {
37        if (this == obj) {
38            return true;
39        }
40        if (obj == null) {
41            return false;
42        }
43        if (!(obj instanceof MutableKey)) {
44            return false;
45        }
46        MutableKey other = (MutableKey) obj;
47        if (i != other.i) {
48            return false;
49        }
50        if (j != other.j) {
51            return false;
52        }
53        return true;
54    }
55}

测试类:

 1public class MutableDemo {
2    public static void main(String[] args{
3        // Object created
4        MutableKey key = new MutableKey(1020);
5        System.out.println("Hash code: " + key.hashCode());
6
7        // Object State is changed after object creation.
8        key.setI(30);
9        key.setJ(40);
10        System.out.println("Hash code: " + key.hashCode());
11    }
12}

输出:

1Hash code: 1291
2Hash code: 1931

只要MutableKey 对象的成员变量i或者j改变了,那么该对象的哈希值改变了,所以该对象是一个可变的对象。


HashMap如何存储键值对

HashMap用Key的哈希值来存储和查找键值对。当插入一个Entry时,HashMap会计算Entry Key的哈希值。Map会根据这个哈希值把Entry插入到相应的位置。查找时,HashMap通过计算Key的哈希值到特定位置查找这个Entry。


HashMap中可变对象作为Key带来的问题

如果HashMap Key的哈希值在存储键值对后发生改变,Map可能再也查找不到这个Entry了。如果Key对象是可变的,那么Key的哈希值就可能改变。在HashMap中可变对象作为Key会造成数据丢失。

下面的例子将会向你展示HashMap中有可变对象作为Key带来的问题:

 1public class MutableDemo1 {
2    public static void main(String[] args) {
3        // HashMap
4        Map<MutableKey, String> map = new HashMap<>();
5        // Object created
6        MutableKey key = new MutableKey(1020);
7        // Insert entry.
8        map.put(key, "Robin");
9        // This line will print 'Robin'
10        System.out.println(map.get(key));
11        // Object State is changed after object creation.
12        // i.e. Object hash code will be changed.
13        key.setI(30);
14        // This line will print null as Map would be unable to retrieve the
15        // entry.
16        System.out.println(map.get(key));
17    }
18}

输出:

1Robin
2null

我们来看看HashMap中get部分源码:

 1public V get(Object key){   
2 // 如果 key 是 null,调用 getForNullKey 取出对应的 value   
3 if (key == null)   
4     return getForNullKey();   
5 // 根据该 key 的 hashCode 值计算它的 hash 码  
6 int hash = hash(key.hashCode());   
7 // 直接取出 table 数组中指定索引处的值,  
8 for (Entry<K,V> e = table[indexFor(hash, table.length)];   
9     e != null;   
10     // 搜索该 Entry 链的下一个 Entry   
11     e = e.next) 
12 {   
13     Object k;   
14     // 如果该 Entry 的 key 与被搜索 key 相同  
15     if (e.hash == hash && ((k = e.key) == key   
16         || key.equals(k)))   
17         return e.value;   
18 }   
19 return null;   
20}   

从源码中我们看到,判断是否找到该对象,我们既判断了key值是否相等,还需要判断他的哈希值是否相同,假如哈希值不相同,根本就找不到我们要找的值。如果Key对象是可变的,那么Key的哈希值就可能改变。在HashMap中可变对象作为Key会造成数据丢失。


如何解决

如果可变对象在HashMap中被用作键,那就要小心在改变对象状态的时候,不要改变它的哈希值了。我们只需要保证成员变量的改变能保证该对象的哈希值不变即可。

在下面的Employee示例类中,哈希值是用实例变量id来计算的。一旦Employee的对象被创建,id的值就不能再改变。只有name可以改变,但name不能用来计算哈希值。所以,一旦Employee对象被创建,它的哈希值不会改变。所以Employee在HashMap中用作Key是安全的。

 1public class MutableSafeKeyDemo {
2    public static void main(String[] args) {
3        Employee emp = new Employee(2);
4        emp.setName("Robin");
5
6        Map<Employee, String> map = new HashMap<>();
7        map.put(emp, "Showbasky");
8
9        System.out.println(map.get(emp));
10
11        emp.setName("Lily");
12        System.out.println(map.get(emp));
13    }
14}
15
16class Employee {
17    private int id;
18    private String name;
19
20    public Employee(final int id) {
21        this.id = id;
22    }
23
24    public final String getName() {
25        return name;
26    }
27
28    public final void setName(final String name) {
29        this.name = name;
30    }
31
32    public int getId() {
33        return id;
34    }
35
36    @Override
37    public int hashCode() {
38        final int prime = 31;
39        int result = 1;
40        result = prime * result + id;
41        return result;
42    }
43
44    @Override
45    public boolean equals(Object obj) {
46        if (this == obj)
47            return true;
48        if (obj == null)
49            return false;
50        if (getClass() != obj.getClass())
51            return false;
52        Employee other = (Employee) obj;
53        if (id != other.id)
54            return false;
55        return true;
56    }
57}

输出:

1Showbasky
2Showbasky

我们只要保证hashcode在使用的过程中不发生改变,就能避免HashMap中可变对象作为Key会造成数据丢失。


同时我们需要遵照以下几点来设计不可变类:

  • 类应该是 final 的,以此来限制子类继承父类,以避免子类改变了父类的不可变特性

  • 类的所有的属性都应该是 final 的

下面我们再来看一段代码,因为子类修改HashCode规则,导致HashMap get数据时数据丢失

 1public class MutableSafeKeyDemo {
2     public static void main(String[] args) {
3            Employee emp = new EmployeeChild(2);
4            emp.setName("Robin");
5
6            // Put object in HashMap.
7            Map<Employee, String> map = new HashMap<>();
8            map.put(emp, "Showbasky");
9
10            System.out.println(map.get(emp));
11
12            // Change Employee name. Change in 'name' has no effect
13            // on hash code.
14            emp.setName("Lily");
15            System.out.println(map.get(emp));
16        }
17    }
18
19    class Employee {
20        // It is specified while object creation.
21        // Cannot be changed once object is created. No setter for this field.
22        public int id;
23        public String name;
24
25        public Employee(final int id) {
26            this.id = id;
27        }
28
29        public final String getName() {
30            return name;
31        }
32
33        public final void setName(final String name) {
34            this.name = name;
35        }
36
37        public int getId() {
38            return id;
39        }
40
41        // Hash code depends only on 'id' which cannot be
42        // changed once object is created. So hash code will not change
43        // on object's state change
44        @Override
45        public int hashCode() {
46            final int prime = 31;
47            int result = 1;
48            result = prime * result + id;
49            return result;
50        }
51
52        @Override
53        public boolean equals(Object obj) {
54            if (this == obj)
55                return true;
56            if (obj == null)
57                return false;
58            if (getClass() != obj.getClass())
59                return false;
60            Employee other = (Employee) obj;
61            if (id != other.id)
62                return false;
63            return true;
64        }
65    }
66
67
68class EmployeeChild extends Employee{
69
70    public EmployeeChild(int id) {
71        super(id);
72    }
73
74    @Override
75    public int hashCode() {
76        final int prime = 31;
77        int result = 1;
78        result = prime * result + ((name == null) ? 0 : name.hashCode());
79        return result;
80    }
81
82}

输出:

1Showbasky
2null

因为修改了父类Employee中成员变量的修饰范围,同时EmployeeChild继承Employee修改了HashCode的规则,导致了最终HashCode不一致,导致取数丢失。

转自:https://mp.weixin.qq.com/s/eBuGJFyFdzsuGkaNcNmz8g