为什么重写了equals方法,就必须重写hashCode

发布时间:2022-02-23 10:41:40 作者:King 来源:本站 浏览量(2526) 点赞(83)
摘要:先来看阿里巴巴Java开发手册中的一段话:【强制】关于 hashCode 和 equals 的处理,遵循如下规则:1) 只要重写 equals,就必须重写 hashCode。2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的 对象必须重写这两个方法。3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和

先来看阿里巴巴Java开发手册中的一段话:

【强制】关于 hashCode 和 equals 的处理,遵循如下规则:1) 只要重写 equals,就必须重写 hashCode。2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的 对象必须重写这两个方法。3) 如果自定义对象作为 Map 的键,那么必须重写 hashCode 和 equals。说明:String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象 作为 key 来使用。

它要求我们若是重写equals方法则必须强制重写hashCode,这是为何呢?

1equals和hashCode方法

我们先来了解一下这两个方法,它们都来自Object类,说明每一个类中都会有这么两个方法,那它俩的作用是什么呢?

首先是equals方法,它是用来比较两个对象是否相等。对于equals方法的使用,得分情况讨论,若是子类重写了equals方法,则将按重写的规则进行比较,比如:

public static void main(String[] args) {
    String s = "hello";
    String str2 = "world";
    boolean result = s.equals(str2);
    System.out.println(result);
}

来看看String类对equals方法的重写:

public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String)anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

由此可知,String类调用equals方法比较的将是字符串的内容是否相等。又如:

public static void main(String[] args) {
    Integer a = 500;
    Integer b = 600;
    boolean result = a.equals(b);
    System.out.println(result);
}

观察Integer类的实现:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

它比较的仍然是值,然而若是没有重写equals方法:

@AllArgsConstructor
static class User {
    private String name;
    private Integer age;
}

public static void main(String[] args) {
    User user = new User("zs"20);
    User user2 = new User("zs"20);
    boolean result = user.equals(user2);
    System.out.println(result);
}

即使两个对象中的值是一样的,它也是不相等的,因为它执行的是Object类的equals方法:

public boolean equals(Object obj) {
    return (this == obj);
}

我们知道,对于引用类型,==比较的是两个对象的地址值,所以结果为false,若是想让两个内容相同的对象在equals后得到true,则需重写equals方法:

@AllArgsConstructor
static class User {
    private String name;
    private Integer age;

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

再来聊一聊hashCode方法,它是一个本地方法,用来返回对象的hash码值,通常情况下,我们都不会使用到这个方法,只有Object类的toString方法使用到了它:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

2为什么只要重写了equals方法,就必须重写hashCode

了解两个方法的作用后,我们来解决本篇文章的要点,为什么只要重写了equals方法,就必须重写hashCode呢?这是针对一些使用到了hashCode方法的集合而言的,比如HashMap、HashSet等,先来看一个现象:

public static void main(String[] args) {
    Map<Object, Object> map = new HashMap<>();
    String s1 = new String("key");
    String s2 = new String("key");

    map.put(s1, 1);
    map.put(s2, 2);
    map.forEach((k, v) -> {
        System.out.println(k + "--" + v);
    });
}

这段程序的输出结果是:key--2,原因是HashMap中的key不能重复,当有重复时,后面的数据会覆盖原值,所以HashMap中只有一个数据,那再来看下面一段程序:

@AllArgsConstructor
@ToString
static class User {
    private String name;
    private Integer age;
}

public static void main(String[] args) {
    Map<Object, Object> map = new HashMap<>();
    User user = new User("zs"20);
    User user2 = new User("zs"20);

    map.put(user, 1);
    map.put(user2, 2);
    map.forEach((k, v) -> {
        System.out.println(k + "--" + v);
    });
}

它的结果应该是什么呢?是不是和刚才一样,HashMap中也只有一条数据呢?可运行结果却是这样的:

EqualsAndHashCodeTest.User(name=zs, age=20)--1
EqualsAndHashCodeTest.User(name=zs, age=20)--2

这是为什么呢?这是因为HashMap认为这两个对象并不相同,那我们就重写equals方法:

@AllArgsConstructor
@ToString
static class User {
    private String name;
    private Integer age;

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

public static void main(String[] args) {
    Map<Object, Object> map = new HashMap<>();
    User user = new User("zs"20);
    User user2 = new User("zs"20);

    System.out.println(user.equals(user2));

    map.put(user, 1);
    map.put(user2, 2);
    map.forEach((k, v) -> {
        System.out.println(k + "--" + v);
    });
}

运行结果:

true
EqualsAndHashCodeTest.User(name=zs, age=20)--1
EqualsAndHashCodeTest.User(name=zs, age=20)--2

两个对象判断是相同的,但HashMap中仍然存放了两条数据,说明HashMap仍然认为这是两个不同的对象。这其实涉及到HashMap底层的原理,查看HashMap的put方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, falsetrue);
}

在存入数据之前,HashMap先对key调用了hash方法:

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

该方法会调用key的hashCode方法并做右移、异或等操作,得到key的hash值,并使用该hash值计算得到数据的插入位置,如果当前位置没有元素,则直接插入,如下图所示:

为什么重写了equals方法,就必须重写hashCode

既然两个对象求得的hash值不一样,那么就会得到不同的插入位置,由此导致HashMap最终存入了两条数据。

接下来我们重写User对象的hashCode和equals方法:

@AllArgsConstructor
@ToString
static class User {
    private String name;
    private Integer age;

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

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

那么此时两个对象计算得到的hash值就会相同:当通过hash计算得到相同的插入位置后,user2便会发现原位置上已经有数据了,此时将触发equals方法,对两个对象的内容进行比较,若相同,则认为是同一个对象,再用新值覆盖旧值,所以,我们也必须重写equals方法,否则,HashMap始终会认为两个new 出来的对象是不相同的,因为它俩的地址值不可能一样。

由于String类重写了hashCode和equals方法,所以,我们可以放心大胆地使用String类型作为HashMap的key。

在HashSet中,同样会出现类似的问题:

@AllArgsConstructor
@ToString
static class User {
    private String name;
    private Integer age;
}

public static void main(String[] args) {
    Set<Object> set = new HashSet<>();
    User user = new User("zs"20);
    User user2 = new User("zs"20);

    set.add(user);
    set.add(user2);

    set.forEach(System.out::println);
}

对于内容相同的两个对象,若是没有重写hashCode和equals方法,则HashSet并不会认为它俩重复,所以会将这两个User对象都存进去。

3总结

hashCode的本质是帮助HashMap和HashSet集合加快插入的效率,当插入一个数据时,通过hashCode能够快速地计算插入位置,就不需要从头到尾地使用equlas方法进行比较,但为了不产生问题,我们需要遵循以下的规则:

  • 两个相同的对象,其hashCode值一定相同
  • 若两个对象的hashCode值相同,它们也不一定相同

所以,如果不重写hashCode方法,则会发生两个相同的对象出现在HashSet集合中,两个相同的key出现在Map中,这是不被允许的,综上所述,在日常的开发中,只要重写了equals方法,就必须重写hashCode。


微信

扫一扫,关注我们

感兴趣吗?

欢迎联系我们,我们愿意为您解答任何有关网站疑难问题!

【如有开发需求】那就联系我们吧

搜索千万次不如咨询1次

承接:网站建设,手机网站,响应式网站,小程序开发,原生android开发等业务

立即咨询 16605125102