Java对象的hashCode和equals方法

参考资料

前言

根据阿里《Java开发手册》,对 Java 对象的 hashCodeequals 方法,有如下强制约定。

[强制] 关于 hashCodeequals 的处理,遵循如下规则

1)只要覆写 equals,就必须覆写 hashCode。

2)因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法。

3)如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。

说明:String 已经覆写 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。

下面进行必要的补充分析。

equals保证可靠性,hashCode保证性能

equals 保证可靠性,hashCode 保证性能。

equalshashCode 都可用来判断两个对象是否相等,但是二者有区别

  • equals 可以保证比较对象是否是绝对相等,即「equals 保证可靠性」
  • hashCode 用来在最快的时间内判断两个对象是否相等,可能有「误判」,即「hashCode 保证性能」
  • 两个对象 equals 为 true 时,要求 hashCode 也必须相等
  • 两个对象 hashCode 为 true 时,equals 可以不等(如发生哈希碰撞时)

hashCode 的「误判」指的是

  1. 同一个对象的 hashCode 一定相等。
  2. 不同对象的 hashCode 也可能相等,这是因为 hashCode 是根据地址 hash 出来的一个 int 32 位的整型数字,相等是在所难免。

此处以向 HashMap 中插入数据(调用 put 方法,put 方法会调用内部的 putVal 方法)为例,对「equals 保证可靠性,hashCode 保证性能」这句话加以说明,putVal 方法中,判断两个 Key 是否相同的代码如下所示。

// putVal 方法
if (p.hash == hash && 
    ((k = p.key) == key || (key != null && key.equals(k))))
...
复制代码

在判断两个 Key 是否相同时,

  1. 先比较 hash(通过 hashCode 的高 16 位和低 16 位进行异或运算得出)。这可以在最快的时间内判断两个对象是否相等,保证性能。
  2. 但是不同对象的 hashCode 也可能相等。所以对满足 p.hash == hash 的条件,需要进一步判断。
  3. 继续,比较两个对象的地址是否相同,== 判断是否绝对相等,equals 判断是否客观相等。

自定义对象作为Set元素时

class Dog {
    String color;

    public Dog(String s) {
        color = s;
    }
}

public class SetAndHashCode {
    public static void main(String[] args) {
        HashSet<Dog> dogSet = new HashSet<Dog>();
        dogSet.add(new Dog("white"));
        dogSet.add(new Dog("white"));

        System.out.println("We have " + dogSet.size() + " white dogs!");

        if (dogSet.contains(new Dog("white"))) {
            System.out.println("We have a white dog!");
        } else {
            System.out.println("No white dog!");
        }
    }
}
复制代码

运行程序,输出结果如下。

We have 2 white dogs!
No white dog!
复制代码

根据阿里《Java开发手册》可知,「因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法」。将 Dog 代码修改为如下。

class Dog {
    String color;

    public Dog(String s) {
        color = s;
    }

    //重写equals方法, 最佳实践就是如下这种判断顺序:
    public boolean equals(Object obj) {
        if (!(obj instanceof Dog))
            return false;
        if (obj == this)
            return true;
        return this.color == ((Dog) obj).color;
    }

    public int hashCode() {
        return color.length();//简单原则
    }
}
复制代码

此时,再运行程序,输出结果如下。

We have 1 white dogs!
We have a white dog!
复制代码

自定义对象作为Map的键和内存溢出

如下代码,自定义 KeylessEntry 对象,作为 Map 的键。

class KeylessEntry {
    static class Key {
        Integer id;
        Key(Integer id) {
            this.id = id;
        }
        @Override
        public int hashCode() {
            return id.hashCode();
        }
    }
    public static void main(String[] args) {
        Map m = new HashMap();
        while (true){
            for (int i = 0; i < 10000; i++){
                if (!m.containsKey(new Key(i))){
                    m.put(new Key(i), "Number:" + i);
                }
            }
            System.out.println("m.size()=" + m.size());
        }
    }
}
复制代码

上述代码中,使用 containsKey(keyElement) 判断 Map 是否已经包含 keyElement 键值。containsKey 的关键代码如下所示,使用了 hashCodeequals 方法进行判断。

if (first.hash == hash && 
    ((k = first.key) == key || (key != null && key.equals(k))))
...
复制代码

执行上述代码,因没有重写 hashCodeequals 方法,导致 m.containsKey(new Key(i)) 判断总是 false,导致程序不断向 Map 中插入新的 key-value,造成死循环,最终将导致内存溢出。

猜你喜欢

转载自juejin.im/post/7103010995970244645
今日推荐