不要使用数组作为HashMap的key——为什么自定义对象需要重写hashcode和equals方法

1,240 阅读2分钟

1. 自定义对象必须要重写hashCode()和equals()方法吗

不是。
所有Java对象都默认会继承Object对象,Obejtc对象有hashCode()和equals(),不过默认的hashCode()方法返回的是内存地址(native方法,所以是物理内存地址还是JMM内存地址不是很清楚,并且应该是对内存地址做了处理),默认的equals()比较的是两个对象的内存地址,即是不是同一个对象。
如果默认的方法满足你的要求,甚至你根本就用不到这两个方法,那可以不重写,只不过你要记着下次用到的时候要再来重写。。。。。。这就是规范让你重写的原因。

2. 重写了equals()方法必须重写hashCode()方法吗

不是。
这两个方法应用的最多的场合就是HashMap,我们来看一下HashMap使用Object作为key进行put操作时发生了什么事呢?

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

使用key的hashCode()值的高16位与低16位做异或运算的结果作为哈希值,确定该对象在HashMap的Entry数组中的索引下标。
假设现有TestObject对象,

public class TestObject {
    String name;
    String description;

    public TestObject(String name) {
        this.name = name;
    }

	@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestObject that = (TestObject) o;
        return Objects.equals(name, that.name);
    }
}

重写了equals()方法,目的是name一样就认为是同一个TestObject对象,不管description属性。我们执行下面的方法:

public static void main(String[] args) {
    TestObject one = new TestObject("aaa");
    TestObject two = new TestObject("aaa");
    HashMap<TestObject, Integer> map = new HashMap<>();
    map.put(one, 1);
    map.put(two, 2);
    System.out.println(one.equals(two));
    System.out.println(map.size());
}

输出:

true
2

明明one和two经过equals比较是相等的,但是在HashMap里面还是两个key,因为hashcode是不等的,HashMap先判断key的hashcode,相等进入Entry数组的同一个下标位置,此时才会比较equals,决定是相同key覆盖还是不同key形成链表。
虽然不是必须,但是看到这里想必你还是能得出结论,最好还是重写一下吧。

3. 重写了hashCode()方法必须重写equals()方法吗

这回的答案真的是,不必。
只不过hashcode重写的不好的话,可能造成大量不同对象的hashcode值一样,从而导致加入hashmap时Entry数组大量位置是空的,而某些位置是非常长的链表(为了严谨,这里提一下链表与红黑树的转换,但是不详细解释,可以去搜索,网上解释很多),非常不好。

4. 为什么不要使用数组作为HashMap的key

终于回到题目。
因为数组不是我们自定义对象,并且数组的hashCode()方法返回的是内存地址。假设我们期待HashMap对数组去重或者统计相同数组数量,这时候就会遇到2中的问题,相同的数组却是不同的key。

5. 补充,HashMap的key还有哪些要求?(面试题)

除了上面的几点,还需要注意HashMap的key最好是不可变对象。
仍以上面的TestObject举例,假设你重写了hashCode和equalis方法:

public class TestObject {
    String name;
    String description;

    public TestObject(String name) {
        this.name = name;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TestObject that = (TestObject) o;
        return Objects.equals(name, that.name);
    }
}

执行下面的代码:

public static void main(String[] args) {
    TestObject one = new TestObject("aaa");
    HashMap<TestObject, Integer> map = new HashMap<>();
    map.put(one, 1);
    System.out.println(map.get(one));
    one.setName("bbb"); 
    System.out.println(map.get(new TestObject("aaa")));
    System.out.println(map.get(new TestObject("bbb")));
    System.out.println(map.get(one));
}

输出:

1
null
null
null

TestObject被修改后,hashcode和equals和原来的对象都不同了,之前存储在HashMap中的TestObject对象被改变了,但是在Enry数组中的位置并不会跟着改变。相当于是按照aaa对象的hashcode值找到Entry数组的对应位置却存了一个bbb对象,这时候无论是原来的对象、还是修改的对象、还是新建的aaa对象、新建的bbb对象都无法找到之前存储在HashMap中的1了。
我们最常用的String作为HashMap的key,因为它是一个不可变对象。