查看原文
其他

吊打面试官系列:说说hashCode和equals方法

田维常 Java后端技术全栈 2021-08-29

关注“Java后端技术全栈

回复“000”获取大量电子书

首先我们需要知道hashCode方法和equals方法都是属于Object类的方法。既然属于Object中public修饰的方法,那言外之就是所有对象默认都有这两个方法,只是有时候有的对象已对这两个方法进行了重写。

hashCode方法是返回一个对象的hash值(int类型),利用对象地址生成一个int类型的数。

equals方法是比较两个对象是否为同一个对象。

两者关系

从本质上来讲,两者是完全能没有什么关系的。但是在某些使用场景下,两者关系非常不一般。

什么场景呢?

比如说作为HashMapHashtable等散列表的key的时候,就是先比较key的hash值,相等再使用equals比较。

关于这个题目网上有很多文章:

这是百度的时候放在第一篇的文章。

谁说equals相等,hashCode就一定相等?

下面我来证明一下

/**
 * 欢迎关注公众号:java后端技术全栈
 *
 * @author 田维常
 * @date 2020/11/16 14:17
 */

public class User {
    private int id;
    private int age;
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //看看我的equals方法有问题吗,完全没毛病
    //当三个属性相等我们就可以认为是同一个人了
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (this == obj) {
            return true;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        User user = (User) obj;
        if (this.getAge() == user.getAge() &&
                this.getId() == user.getId() &&
                this.getName().equals(user.getName())) {
            return true;
        }
        return false;
    }
    //hashcode我们使用默认的
    @Override
    public int hashCode() {
        return super.hashCode();
    }
}

写写个测试类

public class Test {
    public static void main(String[] args) {
        User user1=new User();
        user1.setAge(22);
        user1.setId(1);
        user1.setName("老田");

        User user2=new User();
        user2.setAge(22);
        user2.setId(1);
        user2.setName("老田");

        System.out.println(user1.equals(user2));
        System.out.println("user1 hashcode="+user1.hashCode());
        System.out.println("user2 hashcode="+user2.hashCode());
    }
}

运行测试类

啪啪打脸,你还敢网上随便找答案吗?

在两个方法都没重写的情况下,如果我们想用它作为散列表的key,那么就得确保equals为true的情况下,hashCode一定相等。

这里引入一个面试题:我们可以自定义HashMap的key类吗?

答案是:可以

怎么自定义呢?

像我们上面的User类如果用来作为HashMap的key明显不行。因为HashMap是先使用key的hash值去查找对应的table下表,再通过key的hashCode进行比较,相等的话再比较equals是不是相等。

我们使用上面的User来试试

public class Test {
    public static void main(String[] args) {
        User user1=new User();
        user1.setAge(22);
        user1.setId(1);
        user1.setName("老田");

        User user2=new User();
        user2.setAge(22);
        user2.setId(1);
        user2.setName("老田");

        Map<User,String> map = new HashMap<>();

        map.put(user1,"老田1");
        map.put(user2,"老田2");
        for (Map.Entry<User,String> entry:map.entrySet()){
            System.out.println(entry.getValue());
        }
    }
}

代码运行结果

所以这是不行的,因为user.equasl(user2)是相等的。

关于hashCode方法如何重写,只要满足,两个对象equals相等时,hashCode相等就行。

看看大佬们是如何写的。

Integer中hashCode方法,返回的hashCode就是对应的值。

    @Override
    public int hashCode() {
        return Integer.hashCode(value);
    } 
    public static int hashCode(int value) {
        return value;
    }

比如说:

Integer = 20;

返回的hashCode=20;

再来看看String类

    public int hashCode() {
        //hash值默认为0
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;
            //遍历字符串中每个字符
            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

按照大佬们的玩法,我们也可以模仿着写一个。

自定义hashCode方法

我们把我们上面的User类中的hashCode方法改造一下

    @Override
    public int hashCode() {
        int result = 0;
        result = result * 31 + name.hashCode();
        result = result * 31 + age;
        result = result * 31 + id;
        return result;
    }

再次运行测试类

第二次的put就把第一次put的"老田1"给覆盖了。

上面hashCode为什么这么写呢?

这段描述摘抄自effective java给我们的建议:

1.把某个非零的常数值,比如说0(一个你喜欢的数字),保存在一个名为result的int类型的变量中.

2.对于对象中每个关键域(指equals方法中涉及的每个域),完成以下步骤:

a.为该域计算int类型的散列码c:

i.如果该域是boolean类型,则计算(f?1:0)

ii.如果该域是byte,char,short或者int类型,则计算(int)f.

iii.如果该域是long类型,则计算(int)(f^(f>>>32)).

iv.如果该域是float类型,则计算Float.floatToIntBits(f).

v.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值.

vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode.如果需要更加复杂的比较,则为这个域计算一个"范式",然后针对这个范式调用hashCode.如果这个域的值为null,则返回0(或者其他某个常数,但通常是0).

vii.如果该域是一个数组,则要把每一个元素当做单独的域来处理.也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来.如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法.

b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:

result = 31 * result +c;

3.返回result

从这里我们也得出一个结论:

hashCode方法重写也是有技巧的,不是随便乱写就可以满足的,所以重写的时候一定要慎重。


总结

equals是用来比较对象是否相等的,相等的条件可以自定义,可以定义某个人是猫或狗都可以。

hashCode主要是用在散列表中便于查找存放的位置。

同一个对象如果没有重写equals和hashCode方法时,则equals相等,hashCode也相等。

如果重写了那就啥都不说了,就看你是怎么重写的。

另外阿里巴巴代码规范中:

这里的重写我们可以理解为按照规范重写。不是随便乱写。太主观了就失去意义了。

建议

如果重写equals和hashCode,必须定义一致。如果a.equalse(b)返回true,那么a.hashCode()和b.hashCode()必须有相关的值。

equals和hashcode的关系

  • equals 不相等,hashCode可能相等(hash碰撞)。

  • equals 相等,请重写 hashCode方法,保证 hashCode相等。

关于equals和==,请看另外一篇文章田哥:面试被问== 与equals 的区别,该怎么回答?

推荐阅读:

《程序员面试宝典》.pdf

《分布式Java应用基础与实践》.pdf

《JAVA多线程设计模式》.pdf

    : . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存