写点什么

equals 方法通用约定

作者:李子捌
  • 2021 年 12 月 19 日
  • 本文字数:2907 字

    阅读完需:约 10 分钟

equals方法通用约定

1、简介

Java 程序员都知道 java.lang.Object 类,这是所有类的超类。Object 类中提供了几个 public 的方法,比如:

public boolean equals(Object var1) {        return this == var1;}public String toString() {    return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());}
复制代码

这些 public 方法第一是提供给所有的子类去扩展(覆盖),第二是明确了 Java 中的类所具备的通用约定。因此 Java 中的类在覆盖这些方法是,都需要遵守通用约定,避免程序员们各玩各的。​

那具体应该怎么覆盖,又应该遵守那些通用约定呢?其实这是一个非常复杂的问题,正如 Java 大师约书亚·布洛克(Joshua Bloch)所说:它看似简单,但是往往很多高级程序员也无法完全正确的实现,并且如果不严格遵守,往往会导致非常严重的后果。

2、正文

2.1 什么时候需要重写 equals 方法

总结一句话就是:当我们需要比较两个对象是否“逻辑相等”时,可能需要考虑重写 equals 方法,比如我们需要比较值类型的类 Integer、String,这些类经常需要用于承载和比较值是否相等,或者用于做个 Map、Set 等集合的 Key 值,在这些场景下我们是需要严格的去重写 equals 方法的。(我这里说的是可能,是因为很多情况下无招胜有招,我们或许不需要重写 equals 方法,至于那些场景不需要重写 equals 方法这个会在后面说!)​

2.2 什么时候不需要重写 equals 方法

不需要实现 equals 方法的场景非常多,我们大致的举例说明一下:

  • 类的每个实例唯一。比如说:枚举类型,枚举类型虽然也属于上面说的“值类”,但是由于枚举类的每个值只会存在一个对象,因此不需要重写 equals 方法

  • 类的访问权限是私有的(类私有、包级私有),并且确保 equals 方法不会被调用。说白了就是其他类无法调用到这个类的 equals 方法

  • 超类覆写的 equals 方法,在子类仍然适用。这种情况下我们就无需再多此一举了,在 Java 的 JDK 源码中,set、List、Map 都直接使用了超类的 equals 方法,比如在 HashSet 提供的方法中,并未有 equals 方法的实现,这是因为其父类 AbstractSet 中覆写了 equals 方法,且父类实现的逻辑对于子类也是可用的。


  • 类本身无需提供“逻辑相等”的功能。这种情况其实非常常见,比如我们在实际开发中经常写的工具类,这些类的实例只是用来完成某些任务,并不需要比较它们是否逻辑相等。比如 Java 提供的 java.util.regex.Pattern 类,并未实现 equals 方法,因为它觉得没人会比较两个 Pattern 对象是否相等。


2.3 重写 equals 方法需要遵守哪些规则

重写 equals 方法有几条看起来很简单,但是实现起来几乎无法完全保证的约定:

  1. 自反性(Reflexivity):非 null 情况下,x.equals(x)必须为 true

  2. 对称性(Symmetry):非 null 情况下,x.equals(y) = true 则 y.equals(x) = true

  3. 传递性(Transitive):非 null 情况下,x.equals(y) = true && y.equals(z) = true 则 x.equals(z) = true

  4. 一致性(Consistent):非 null 情况下,x.equals(y) = true 只要 x 或 y 其中任意一个对象不被修改,那么 x.equals(y) = true 应该恒成立

  5. 非空性(Non-nullity):x 不为 null 的情况下,x.equals(null)必须返回 false


看到这五条规则是不是觉得头大,平时我们在写的时候,压根就没考虑过这么多条条框框,只有能实现功能上的逻辑相等了就行!其实我觉得这么想也不能说是完全不对,因为如果一定要完完全全的按照它这个规范来,那么面向对象很多功能都用不了了,比如说继承。其实 Java 的 JDK 中也是有些代码不满足上面说的这五条规范的,比如我们看下如下这段代码(猜猜它会输出什么?):

package com.lizba.tips;import java.sql.Timestamp;import java.util.Date;/** * <p> *    Java自带jdk equals方法的对称性测试 * </p> * * @Author: Liziba * @Date: 2021/10/24 14:48 */public class EqualsDemo {    public static void main(String[] args) {        Date date = new Date();        Timestamp timestamp = new Timestamp(date.getTime());        System.out.println("Date equals to Timestamp: " + date.equals(timestamp));        System.out.println("Timestamp equals to Date: " + timestamp.equals(date));    }}
复制代码

是的你没看错,第一个输出了 true,第二个输出了 false。很显然这不满足第二点:对称性(Symmetry)。

Date equals to Timestamp: trueTimestamp equals to Date: false
复制代码

基于这种情况,Java 并没有很好的办法去解决。只能说告诉你不用混用 Date 和 Timestamp,并且无论如何不要去 equals 比较 Date 和 Timestamp,这个在 Timestamp 中的类和 equals 方法上也是有说明的!​


2.4 实现高质量 equals 方法的诀窍

上面聊了一些什么时候需要重写 equals 方法、什么时候不需要重写 equals 方法、重写 equals 方法需要遵守的规则。这里我们聊一聊实现高质量 equals 方法的诀窍。在这里我将会引用 java.util.AbstractSet 类中的 equals 方法来阐述如何写一个高质量的 equals 方法,因为小捌发现它非常经典。

java.util.AbstractSet 中的 equals 方法:

public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {        // ...        public boolean equals(Object o) {        if (o == this)            return true;        if (!(o instanceof Set))            return false;        Collection<?> c = (Collection<?>) o;        if (c.size() != size())            return false;        try {            return containsAll(c);        } catch (ClassCastException unused)   {            return false;        } catch (NullPointerException unused) {            return false;        }    }        // ...    }
复制代码

第一点:o == this 使用==操作符,判断比较对象和当前对象的引用是否相等,如果相等代表同一个对象,那就直接返回 true。​

第二点:o instanceof Set 通过 instanceof 操作符检查参数类型是否正确,如果类型都不对就不需要比较了。​

第三点:c.size() != size()这是在 Abstract 中的特殊存在,并不是所有的都需要这样比较,提前比较大小的好处是无需进行每个域的比较,如果大小都不相等,就可以直接返回了。通常情况下,这样做性能更好!​

第四点:containsAll(c)对该类中的每一个域进行比较,如果所有的域都相等则返回 true,如果不相等返回 false。​

第五点:重写 hashcode 重写 equals 方法时一定要重写 hashcode 方法,比如 java.util.AbstractSet 中重写了 hashCode()方法,它将每个域的 hashcode 进行了拼接

public int hashCode() {    int h = 0;    Iterator<E> i = iterator();    while (i.hasNext()) {        E obj = i.next();        if (obj != null)            h += obj.hashCode();    }    return h;}
复制代码


第六点:不要修改 equals(Object o)的参数类型这一点看似很简单,但是如果你不是用 IDE 自动生成的 equals 方法,而是自己手动敲得代码,很容易会将 Object 类型,改成当前类的类型,这种做法是不对的哈!因为这不是重写(Override),这是重载(Overload)

发布于: 10 小时前阅读数: 6
用户头像

李子捌

关注

华为云享专家 2020.07.20 加入

公众号【李子捌】

评论

发布
暂无评论
equals方法通用约定