如何编写出高质量的 equals 和 hashcode 方法?

什么是 equals 和 hashcode 方法?

这要从 Object 类开始说起,我们知道 Object 类是 Java 的超类,每个类都直接或者间接的继承了 Object 类,在 Object 中提供了 8 个基本的方法,equals 方法和 hashcode 方法就是其中的两个。

equals 方法:Object 类中的 equals 方法用于检测一个对象是否等于另一个对象,在 Object 类中,这个方法将判断两个对象是否具有相同的引用,如果两个对象具有相同的引用,它们一定是相等的。

hashcode 方法:用来获取散列码,散列码是由对象导出的一个整数值,散列码是没有规律的,如果 x 和 y 是两个不同的对象,那么 x.hashCode() 与 y.hashCode() 基本上不会相同

为什么要重写 equals 和 hashcode 方法?

为什么需要重写 equals 方法和 hashcode 方法,我想主要是基于以下两点来考虑:

1、我们已经知道了 Object 中的 equals 方法是用来判断两个对象的引用是否相同,但是有时候我们并不需要判断两个对象的引用是否相等,我们只需要两个对象的某个特定状态是否相等。比如对于两篇文章来说,我只要判断两篇文章的链接是否相同,如果链接相同,那么它们就是同一篇文章,我并不需要去比较其它属性或者引用地址是否相同。

2、在某些业务场景下,我们需要使用自定义类作为哈希表的键,这时候我们就需要重写,因为如果不做特定修改的话,每个对象产生的 hashcode 基本上不可能相同,而 hashcode 决定了该元素在哈希表中的位置,equals 决定了判断逻辑,所以特殊情况下就需要重写这两个方法,才能符合我们的要求。

我们使用一个小 Demo 来模拟一下特殊场景,让我们更好的理解为什么需要重写 equals 和 hashcode 方法,我们的场景是:我们有很多篇文章,我需要判断文章是否已经存在 Set 中,两篇文章相同的条件是访问路径相同。

好了,我们一起动手写 Demo 吧,我们建立一个文章类来存放文章信息,文章类具体设计如下:

class Article{
    // 文章路径
    String url;

    // 文章标题
    String title;
    public Article(String url ,String title){
        this.url = url;
        this.title = title;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
}

文章类中有路径、标题两个属性,在这个类中我们并没有重写 equals 和 hashcode 方法,所以这里会使用超类 Object 中的 equals 和 hashcode 方法,为了防止你没有看过 Object 类中的 equals 和 hashcode 方法,我们先一起来看一下 Object 的类中的 equals 和 hashcode 方法:

看完之后,接下来,我们编写一个测试类,测试类代码如下:

public class EqualsAndHashcode {
    public static void main(String[] args) {
        Article article = new Article("www.baidu.com","百度一下");
        Article article1 = new Article("www.baidu.com","坑B百度");

        Set<Article> set = new HashSet<>();
        set.add(article);
        System.out.println(set.contains(article1));

    }
}

在测试类中,我们实例化了两个文章对象,文章对象的 url 都是一样的,标题不一样,我们将 article 对象存入到 Set 中,判断 article1 对象是否存在 Set 中,按照我们的假设,两篇文章的 Url 相同,则两篇文章就应该是同一篇文章,所以这里应该给我们返回 True,我们运行 Main 方法。得到结果如下:


我们看到了结果不是你想要的 True 而是 False ,这个原因很简单,因为两篇文章的访问路径相同就是同一篇文章,这是我们定义的规则,我们并没有告诉我们的程序这个规则,我们没有重写 equals 和 hashcode 方法,所以系统在判断的时候使用的是 Object 类默认的 equals 和 hashcode 方法,默认的 equals 方法判断的是两个对象的引用地址是否相同,这里肯定是不一样的,得到的答案就是 False 。我们需要把相等的规则告诉我们的程序,那我们就把 equals 方法重写了。

1、重写 equals 方法

在这里我们先使用 IDEA 工具生成的 equals 方法,把最后的逻辑返回逻辑修改一下就好了,具体的编写规则我们下面会介绍。最后我们的 equals 方法如下

/**
 * 重写equals方法,只要两篇文章的url相同就是同一篇文章
 * @param o
 * @return
 */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Article article = (Article) o;
        return Objects.equals(url, article.url);
    }

再一次运行 Main 方法,你会发现还是 False ,这是为什么呢?我已经把判断两个对象相等的逻辑告诉程序了,不急,我们先来聊一聊哈希表吧,我们知道哈希表采用的是数组+链表的结构,每个数组上挂载着链表,链表的节点用来存储对象信息,而对象落到数组的位置由 hashcode()。所以当我们调用 HashSet 的 add(Object o) 方法时,首先会根据o.hashCode()的返回值定位到相应的数组位置,如果该数组位置上没有结点,则将 o 放到这里,如果已经有结点了, 则把 o 挂到链表末端。同理,当调用 contains(Object o) 时,Java 会通过 hashCode()的返回值定位到相应的数组位置,然后再在对应的链表中的结点依次调用 equals() 方法来判断结点中的对象是否是你想要的对象。

由于我们只重写了 equals 方法并没有重写 hashcode 方法,所以两篇文章的 hashcode 值不一样,这样映射到数组的位置就不一样,调用 set.contains(article1) 方法时,在哈希表中的情况可能如下图所示:

article 对象被映射到了数组下标为 0 的位置,article1 对象被映射到了数组下标为 6 的位置,所以没有找到返回 False。既然只重写 equals 方法不行,那么我们把 hashcode 方法也重写了。

2、重写 hashcode 方法

跟 equals 方法一样,我们也使用 idea 编辑器帮我们生成的 hashcode 方法,只需要做稍微的改动就可以,具体 hashcode 代码如下:

    @Override
    public int hashCode() {
        return Objects.hash(url);
    }

重写好 hashcode 方法之后,再一次运行 Main 方法,这次得到的结果为 True,这会就是我们想要的结果了。重写 equals 和 hashcode 方法之后,在哈希表中的查找如下图所示:

首先 article1 对象也会被映射到数组下标为 1 的位置,在数组下标为 1 的位置存在 article 数据节点,所以会执行 article1.equals(article) 命令,因为我们重写了 Article 对象的 equals 方法,这个是否会判断两个 Article 对象的 url 属性是否相等,如果相等就返回 True,在这里显然是相等的,所以这里就返回 True,得到我们想要的结果。

如何编写 equals 和 hashcode 方法?

需要自己重写 equals 方法?好的,我这就重写,噼里啪啦的敲出了下面这段代码:

public boolean equals(Article o) {
    if (this == o) return true;
    if (o == null || !(o instanceof  Article)) return false;
    return o.url.equals(url);
}

这样写对吗?虽然里面的逻辑看上的没什么问题,但是 equals 方法的参数变成了Article。 其实你这跟重写 equals 方法没有半毛线关系,这完全是重新定义了一个参数类型为 Article 的 equals 方法,并没有去覆盖 Object 类中的 equals 方法。

那该如何重写 equals 方法呢?其实 equals 方法是有通用规定的,当你重写 equals 方法时,你就需要重写 equals 方法的通用约定,在 Object 中有如下规范: equals 方法实现了一个等价关系(equivalence relation)。它有以下这些属性:

  • 自反性:对于任何非空引用 x,x.equals(x) 必须返回 true
  • 对称性:对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true
  • 传递性:对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,则 x.equals(z) 必须返回 true
  • 一致性:对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用必须始终返回 true 或始终返回 false
  • 非空性:对于任何非空引用 x,x.equals(null) 必须返回 false

现在我们已经知道了写 equals 方法的通用约定,那我们就参照重写 equals 方法的通用约定,再一次来重写 Article 对象的 equals() 方法。代码如下:

    // 使用 @Override 标记,这样就可以避免上面的错误
    @Override
    public boolean equals(Object o) {
        // 1、判断是否等于自身
        if (this == o) return true;
        // 2、判断 o 对象是否为空 或者类型是否为 Article
        if (o == null || !(o instanceof  Article)) return false;
        // 3、参数类型转换
        Article article = (Article) o;
        // 4、判断两个对象的 url 是否相等
        return article.url.equals(url);
    }

这一次我们使用了 @Override 标记,这样就可以避免我们上一个重写的错误,因为父类中并没有参数为 Article 的方法,所以编译器会报错,这对程序员来说是非常友好的。接下来我们进行了 自反性、非空性的验证,最后判断两个对象的 url 是否相等。这个 equals 方法就比上面那个要好很多,基本上没什么大毛病了。

在 effective-java 书中总结了一套编写高质量 equals 方法的配方,配方如下:

  • 1、使用 == 运算符检查参数是否为该对象的引用。如果是,返回 true。
  • 2、使用 instanceof 运算符来检查参数是否具有正确的类型。 如果不是,则返回 false。
  • 3、参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功。
  • 4、对于类中的每个「重要」的属性,请检查该参数属性是否与该对象对应的属性相匹配。

我们已经了解了怎么重写 equals 方法了,接下来就一起了解如何重写 hashcode 方法,我们知道 hashcode 方法返回的是一个 int 类型的方法,那好办呀,像下面这样重写就行了

@Override
 public int hashCode() {
 return 1;
 }

这样写对吗?对错先不管,我们先来看一下 hashcode 在 Object 中的规定:

  • 1、当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。
  • 2、如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
  • 3、如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。

照 hashcode 规定来看,这样写似乎也没什么问题,但是你应该知道哈希表,如果这样写的话,对于HashMap 和 HashSet 等散列表来说,直接把它们废掉了,在哈列表中,元素映射到数组的哪个位置靠 hashcode 决定,而我们的 hashcode 始终返回 1 ,这样的话,每个元素都会映射到相同的位置,散列表也会退化成链表。

结合 hashcode 的规范和散列表来看,要重写出一个高质量的 hashcode 方法,就需要尽可能保证每个元素产生不同的 hashcode 值,在 JDK 中,每个引用类型都重写了 hashcode 函数,我们看看 String 类中的 hashcode 是如何重写的:

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;
}

这个 hashcode 方法写的还是非常好的,我个人比较喜欢用官方的东西,我觉得他们考虑的肯定比我们多很多,所以我们 Article 类的 hashcode 方法就可以这样写

    /**
     * 重写 hashcode方法,根据url返回hash值
     * @return
     */
    @Override
    public int hashCode() {
        return url.hashCode();
    }

我们直接调用 String 对象的 hashcode 方法。到此我们的 equals 方法和 hashcode 方法都重写完了,最后以 effective-java 里面的一段总结结尾吧。

  • 1、当重写 equals 方法时,同时也要重写 hashCode 方法
  • 2、不要让 equals 方法试图太聪明。
  • 3、在 equal 时方法声明中,不要将参数 Object 替换成其他类型。

文章不足之处,望大家多多指点,共同学习,共同进步

最后

打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一起进步吧。

原文地址:https://www.cnblogs.com/jamaler/p/11595868.html

时间: 2024-10-14 12:09:54

如何编写出高质量的 equals 和 hashcode 方法?的相关文章

编写OC高质量的代码的有效方法

1. 写这个只是为了自己记忆,有相关pdf文件,如需要留下邮箱.. 2. 在类的头文件中尽量少引入其他头文件 除非确有必要,否则不要引入头文件.一般来说,应在某个类的头文件中使用向前声明来提及别的类(使用@class),并在实现文件中引入那些类的头文件,这样做可以尽量降低类之间的耦合. 如果要声明某个类遵循某个协议,应该把这个协议放到分类中,或者把协议单独放在一个头文件中,然后将其引入. 3. 多用字面量语法,少用与之等价的方法 下面是两种方式的对比: // 使用字面量语法的例子 NSArray

10个编程小技巧,教你写出高质量代码!

你会写代码吗你会写高质量代码吗你知道怎么写高质量代码吗不要一上来就开始写代码想清楚,再动手今天,分享10个写代码的小技巧教你写出高质量代码↓↓↓ 1.重构思维模式 不要一上来就开始写代码,要掌握尽量多的重构方法,重构思维方式,掌握重构并不一定是要对原来代码的重构,而是让自己在操作之前就想好该怎么去进行. 2.搞清需求再动手 看到需求之后,肯定多多少少会有一些问题,或是理解上的错误,或是功能实现上的问题,这时,必须要交流清楚,否则,后续将会有更多问题. 3.文档也要写 可能不少人觉得文档没人看,写

为什么国人很难出高质量开源

FKP-REST是一套全栈javascript框架 为什么国人很难出高质量开源 作者:webkixi 乱侃 今天因为VueJS展开的讨论,话锋转转,就转到这个主题了.这个主题其实也一直是我想要了解的问题,毕竟是 这个FKPJS要走的路.怎么样才能做好开源. 这里先推荐一篇好文,<思考的八种境界>,文章不长,但却有些感悟,第一条就撞在胸口, <一.形成主见>,行文大概意思是:有自己的一套,即便是很浅薄. 是的,即便浅薄,那也是心血所成,正如我正在做的FKPJS,虽然浅薄,但却花费了我

如何写出高质量的技术博客 这边文章出自http://www.jianshu.com/p/ae9ab21a5730 觉得不错直接拿过来了 好东西要大家分享嘛

    如何写出高质量的技术博客?答案是:如果你想,就一定能写出高质量的技术博客.看起来很唯心,但这就是事实.有足够愿力去做一件目标明确,有良好反馈系统的事情往往很简单.就是不停地训练,慢慢地,你自己就能找出规律和技巧.所以,要写出高质量的技术博客,首先要解决为什么要写的问题. 为什么要写 我一直很喜欢的一个学习方法是 Learning by teaching 一个课题,如果你能给不懂的人解释清楚,说明你对这个课题的理解足够深入.把一个课题展开来写,你可能会发现某些方面你还写不清楚,这往往说明你

4年前的随笔---写出高质量程序的要点

从1990年開始敲代码.到如今已经快20年了.总结出写出高质量程序的几个要点: - 1.開始写之前思路越清晰完整越好. - 2.写的过程中代码一定要规范一致,这种代码便于维护和改动.这个规范一致性包括名称.格式.算法等.- 3.发现一处错误,马上回忆有没有可能其他地方具有相同的错误(假设你遵循第2条.就能非常快找到). - 4.多用ASSERT,在我的代码里面,这条语句至少占领了程序总量的1/10. - 5.每写完一段后至少重复看3遍.非常多BUG是非常难用调试器找出来的. - 通过遵循以上规则

出高质量优质短效IP,爬虫代理

出高质量优质短效IP,爬虫代理 我们IP池量大,重复IP少,业务独享,下面产品介绍!http://www.16yun.com本人的联系方式:QQ664014706还可免费测试,欢迎咨询!!! 原文地址:https://www.cnblogs.com/wchengdu97c/p/8945969.html

小白程序员怎么由量变到质变写出高质量代码

小白程序员怎么由量变到质变写出高质量代码?很多老程序员从事开发多年,有这样一种感觉,查看一些开源项目,如Spring.Apache Common等源码是一件赏心悦目的事情,究其原因,无外两点: 1.代码质量非常高; 2.命名特别规范: 要写高质量的代码,不是一件容易的事,需要长年累月的锻炼,是一个量变到质变的过程,但要写好命名,只需要有比较好的英语语法基础和一种自我意识即可轻松达到. 1.切忌使用没有任何意义的英语字母进行命名. 2.切忌使用拼音,甚至是拼音首字母组合. 3.要使用英文,而且要使

Java中的equals和hashCode方法

本文转载自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法. equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复.这里我们首先要明白一个问题: equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的h

Java中的equals和hashCode方法详解

Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这两个方法,今天就来介绍一些这两个方法的作用. equals()和hashCode()方法是用来在同一类中做比较用的,尤其是在容器里如set存放同一类对象时用来判断放入的对象是否重复. 这里我们首先要明白一个问题: equals()相等的两个对象,hashcode()一定相等,equals()不相等的两个对象,却并不能证明他们的hashcode()不相等.换