面试高频考点:hashCode与equals

共 6553字,需浏览 14分钟

 ·

2021-12-23 14:19

点击关注公众号,Java干货及时送达

作者:随身电源 

来源:https://juejin.cn/post/7011713684015677471


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

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

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

equals和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());
}

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

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

public static void main(String[] args) {
    Map 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 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 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值计算得到数据的插入位置,如果当前位置没有元素,则直接插入,如下图所示:既然两个对象求得的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 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对象都存进去。

总结

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

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

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


1、Log4j2维护者吐槽没工资还要挨骂,GO安全负责人建议开源作者向公司收费
2、太难了!让程序员崩溃的8个瞬间
3、2021年程序员们都在用的神级数据库
4、Windows重要功能被阉割,全球用户怒喷数月后微软终于悔改
5、牛逼!国产开源的远程桌面火了,只有9MB 支持自建中继器!
6、摔到老三的 Java,未来在哪?
7、真香!用 IDEA 神器看源码,效率真高!

点分享

点收藏

点点赞

点在看

浏览 14
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报