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

什么是 equals 和 hashcode 方法?

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

equals 方法:Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象,在 Object 类中,这个方法将判断两个对象是否具备相同的引用,若是两个对象具备相同的引用,它们必定是相等的。程序员

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

为何要重写 equals 和 hashcode 方法?

为何须要重写 equals 方法和 hashcode 方法,我想主要是基于如下两点来考虑:微信

一、咱们已经知道了 Object 中的 equals 方法是用来判断两个对象的引用是否相同,可是有时候咱们并不须要判断两个对象的引用是否相等,咱们只须要两个对象的某个特定状态是否相等。好比对于两篇文章来讲,我只要判断两篇文章的连接是否相同,若是连接相同,那么它们就是同一篇文章,我并不须要去比较其它属性或者引用地址是否相同。编辑器

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

咱们使用一个小 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 方法重写了。

一、重写 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 方法也重写了。

二、重写 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) {
        // 一、判断是否等于自身
        if (this == o) return true;
        // 二、判断 o 对象是否为空 或者类型是否为 Article 
        if (o == null || !(o instanceof  Article)) return false;
        // 三、参数类型转换
        Article article = (Article) o;
        // 四、判断两个对象的 url 是否相等
        return article.url.equals(url);
    }
复制代码

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

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

  • 一、使用 == 运算符检查参数是否为该对象的引用。若是是,返回 true。
  • 二、使用 instanceof 运算符来检查参数是否具备正确的类型。 若是不是,则返回 false。
  • 三、参数转换为正确的类型。由于转换操做在 instanceof 中已经处理过,因此它确定会成功。
  • 四、对于类中的每一个「重要」的属性,请检查该参数属性是否与该对象对应的属性相匹配。

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

@Override
 public int hashCode() { 
 return 1; 
 }
复制代码

这样写对吗?对错先无论,咱们先来看一下 hashcode 在 Object 中的规定:

  • 一、当在一个应用程序执行过程当中,若是在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另外一个应用程序的每一次执行返回的值能够是不一致的。
  • 二、若是两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
  • 三、若是两个对象根据 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 里面的一段总结结尾吧。

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

文章不足之处,望你们多多指点,共同窗习,共同进步

最后

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

平头哥的技术博文
相关文章
相关标签/搜索