为什么说重写equals方法一定要重写hashCode方法?

导语:在进行知识讲解这前 我们先了解以下知识 在jvm层面来说 我们每一个对象都有hashCode 即hash散列码 他是对象的身份证 hashCode方法是Java本地方法(当然也在Object中定义) equals方法是Java基类Object中定义的方法 为什么要在Object中定义它两?原因很简单 因为每一个类都直接或间接继承了Object类 在Object定义的方法 保证所有类都有这两个方法 在执行效率上来说 hashCode 肯定比equals方法更高的 了解以上知识后那么我们就可以去探讨为什么我们重写equals方法之后一定要重写hashCode了

Ⅰ.探索equals的真面目

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

这就是来自超类Object中对equals的定义 很简单 如果两个实例的引用相等 那就返回true 其实问题就出在这里 很多时候我们更多的是希望我们认为的相等不是它们的引用相等 而是他们的值相等(更严谨的说是字面量相等)那么 基于这一点 我们可以 覆盖(重写)来自超类的equals方法

举个栗子:

@Data
public class Person {

    private Integer age;

    private String name;
    
    private String idCard;

    public String getPersonInfo(Person person){
        return String.format("age:%s\tname:%s\tidCard:",person.age,person.name,person.idCard);
    }

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
}

比如我们有这么一个类 现在我们的需求是 只要他们的身份证号相等 那它们就是相等的对象 于是我们重写equls方法

@Data
public class Person {

    private Integer age;

    private String name;

    private String idCard;

    public String getPersonInfo(Person person){
        return String.format("age:%s\tname:%s\tidCard:",person.age,person.name,person.idCard);
    }

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public boolean equals(Person person) {
        return (this.idCard == person.idCard);
    }
}

细心的网友一定发现了 其实上述这个问题是很多Java新人常犯的错误 请注意上面的代码是错的也不能说他是错的 只能说它并没有重写来自超类的equals方法 而是重载了一个equals方法 很多人要吐了 这又是重写又是重载的 要裂开了 别着急 听我慢慢道来 所谓重载就是每一个Java客户端类允许同名方法存在 但是这些同名方法的方法签名不能相同 什么叫方法签名呢? 注意方法签名指的是 例如上述 equals(Person person) 需要注意的是方法签名不包括 方法返回值 所以 public boolean equals(Person person) ,public void equals(Person person) 它们并不是重载方法 所以它们不能在Java类中同时存在 这里希望大家可以自己试试增加印象 所谓重写就是子类重写父类的方法 所以它涉及到两个类 重写和重载都是Java多态的体现 说完这些 我们在回过头去看我们刚刚想重写的方法时就会发现 我们重写的不对啊 因为 equals(Person person) 与 equals(Object obj) 方法签名不相同 所以我们并没有重写来自超类的方法 而是重载了一个与超类相同名称但方法签名不同的方法而已 那正确的重写应该长下面这样

@Data
public class Person {

    private Integer age;

    private String name;

    private String idCard;

    public String getPersonInfo(Person person) {
        return String.format("age:%s\tname:%s\tidCard:", person.age, person.name, person.idCard);
    }

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public boolean equals(Object obj) {

        /**
         * 哈哈哈哈这里肯定相等啦 由自旋性我们可以知道 自己肯定等于自己或者说引用相等 表面指向同一个对象 那100%相等
         * 这样写可以及时返回结果 不必要进行下面的强制类型转换去判断 当然不加这个if代码没有问题
         */
        if (this == obj) {
            return Boolean.TRUE.booleanValue();
        }
        if (obj instanceof Person) {
            Person person = (Person) obj;
            return (this.idCard == person.idCard);
        } else {
            return Boolean.FALSE.booleanValue();
        }
    }
}

怎么有效规避犯上面的错误呢?我们可以借助Java注解来解决 显示的告诉编译器我们是想要重写这一方法 加上@Override 这个时候如果你再写错写成了上面那个错误的重载 那么编辑器就会飘红告诉你 小老弟你错了 你这不是重写

就是这样咯:

   @Override
    public boolean equals(Object obj) {

        /**
         * 哈哈哈哈这里肯定相等啦 由自旋性我们可以知道 自己肯定等于自己或者说引用相等 表面指向同一个对象 那100%相等
         * 这样写可以及时返回结果 不必要进行下面的强制类型转换去判断 当然不加这个if代码没有问题
         */
        if (this == obj) {
            return Boolean.TRUE.booleanValue();
        }
        if (obj instanceof Person) {
            Person person = (Person) obj;
            return (this.idCard == person.idCard);
        } else {
            return Boolean.FALSE.booleanValue();
        }
    }

如果你在刚刚那个重载的方法上加上@Override 就会飘红
由于你不是重写,加上这个重写注解就会飘红 可以避免出现假重写的尴尬

朋友们,我们上面重写了equals方法并没有去重写hashCode方法这会对我们未来的编程带来什么问题呢?带着这个疑问我们一起去探索hashCode方法的奥秘吧

Ⅱ.探索hashCode方法的奥秘

 public native int hashCode();

看到没有 这就是来自超类Object类的hashCode 底层由C++实现 是非常著名的Java本地方法 一切基于hash散列函数实现的例如 hashMap hashTable hashSet 等底层都是依赖hashCode函数的 那这些集合的实现为什么要基于hashCode呢? 这么说吧 例如我们的hashMap 我们都知道hashMap中不允许Key相同的元素存在 那它到底是怎么去衡量(或者说保证)两个element的key不相同的呢? 那其实是这样的

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

上面是hashMap的put方法 我们可以很明显看到 每一次插入元素这前都要进行一次hash求值 如果hash值不相等 那么就直接插入 因为我们都知道**【两个相等的对象它们的hash一定相等 两个不同的对象hash可能相等 如果两个对象hash值不相等那肯定不相等】**上述是通用的hash约定 我们可以在sun官网上的开发者文档上找到 如果hash相等 那就说明发生了hash冲突 hashMap会解决冲突(jdk1.7基于链表解决 jdk1.8以后引入了红黑树 这边不再展开来讲) 这个时候我们hashMap就会问?真的相等吗?接着就会去调用equals方法判断到底是不是真的相等 如果真的相等 存在相同Key元素就覆盖已存在元素 那有人就会问了 那搞了一大圈为什么我们不直接调用equals方法呢? 原因很简单 因为hashCode底层是基于C++实现的 效率明显高于equals 我们hashMap就是希望尽可能少调用equals去完成map操作

基于上面我讲的这些我想再去看我上诉那个例子大家很快就会知道 为什么我们只重写erqals方法是会有问题的 在什么时候会有问题 在我们使用基于hash散列函数实现的集合框架会有问题 你不重写hashCode会导致你基于Person类使用hashMap等不能正常工作 幸运的是 Java 中很多类都帮我们重写了equals方法和hashCode方法 例如 著名的String String在Java中的地位那真是太显赫了 下次有时间一定要好好讲讲String

 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 int hashCode() {
        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;
    }

以上是String重写的hashCode 方法

总结:hashCode()的默认行为本质上是对堆上的对象产生独特值(你可以认为就是对象的身份证号)。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)如果我们重写了equals方法而不去重写hashCode方法 将会永远也得不到两个相等的对象具有相等的hash通用约定 值得注意: 一个好的hashCode生成算法应该尽量减少hash冲突发生的概率 即应该尽可能的将hash散列值发散在一个足够大且散的映射空间内以保证减少hash碰撞的概率

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值