Effective Java 【对于所有对象都通用的方法】第10条 覆盖equals方法请遵守通用规范

覆盖equals方法请遵守通用规范

尽管Object是一个具体类,但是设计它主要是为了扩展,它所有的非final方法(equals、hashCode、toString、clone、finalize)都有明确的通用约定,因为它们设计成是可覆盖的(override),任何一个类,它在覆盖这些方法的时候,都有责任遵守这些通用约定,如果不能做到这点,其他依赖于这些约定的类(例如:HashMap和HashSet)就无法结合该类一起正常运作。

覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重,最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等,如果满足以下任何一个条件,这就正是所期望的结果(就不用覆盖equals方法):

  • 类的每个实例本质上是唯一的。对于代表活动实体而不是值的类确实如此,例如Thread,Object提供的equals实现对于这些类来说正是正确的行为。
  • 类没有必要提供“逻辑相等“的测试功能。例如:java.util.regex.Pattern可以覆盖equals方法,已检查两个Pattern实例是否代表同一个正则表达式,但是设计者并不认为客户端需要或者期望这样的功能。在这类情况下,从Object继承的得到的equals实现就足够了。
  • 超类已经覆盖了equals,超类的行为对于这个类也是合适的。例如,大多数的Set实现都从AbstractSet继承equals实现,Map实现从AbsrtractMap继承equals实现。
  • 类是私有的,或者是包级私有的,可以确保它的equals方法永远不会被调用。如果非常想要避免风险,可以覆盖equals方法,并加以控制,以确保不会被意外调用

那么,什么时候应该覆盖equals方法呢?如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals方法,这通常属于值类的情形。值类仅仅是一个表示值的类,例如:Integer或者String,程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是像了解它们是否指向同一个对象,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用作映射表(map)的键(key)或者集合(set)的元素,使映射或者集合表现出逾期的行为。

有一种"值类"不需要覆盖equals方法,即用实例受控确保"每个值最多只存在一个对象"的类,枚举类型就属于这种类,对于这样的类而言,逻辑相同与对象相同是一回事。

在覆盖equals方法的时候,必须遵守通用约定,下面是约定的内容,来自Object的规范

  • 自反性:对于任何非null的引用值x,x.equals(x),必须返回true。
  • 对称性:对于任何非null的引用值x、y,当且仅当x.equals(y)返回true时,y.equals(x)也必须返回true。
  • 传递性:对于任何非null的引用值x、y、z,如果x.equals(y)返回true,y.equals(z)也返回true,那么x.equals(z)也必须返回true。
  • 一致性:对于任何非null引用值x、y,只要equals的比较操作在对象中所用的信息没有修改,多次调用x.equals(y)就会一致的返回true,或者一致的返回false。
  • 非空性:对于任何非null引用值x,x.equals(null)必须返false。

这里列举实现高质量equals方法的几个注意点:

  • 1.使用==操作符检查”参数是否为这个对象的引用。如果是,则返回true,这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
  • 2.使用instanceof操作符检查”参数是否为正确的类型。如果不是,则返回false。一般来说,所谓“正确的类型”是指equals方法所在的那个类。某些情况下,是指该类所实现的某个接口。如果类实现的接口进行了equals约定,允许在实现了该接口的类之间进行比较,那就使用接口,集合接口如Set,List,Map和Map.Entry具有这样的特性。
  • 3.把参数转换正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
  • 4.对于该类的每个关键域,检查参数中的域是否与该对象中的对应的域相匹配。如果这些测试全部成功,则返回true,否则返回false,如果第2步中的类型是个接口,就必须接口方法访问参数中的域,如果该类型是一个类,也许就能够直接访问参数中的域,这要取决与它们的可访问性。

对于第四点多做一些说明:对于既不是float也不是double类型的基本类型域,可以直接使用==操作符进行比较,对于对象域,可以递归调用equals方法,对于float域,可以使用静态Float.compare(float f1,float f2)方法;对于double域,则使用Double.compare(double d1,double d2)。对float和double域进行特殊的处理是有必要的,因为存在着Float.NaN、-0.0f以及类似的double常量;虽然可以用静态方法Float.equals和Double.equals对float和double域进行比较,但是每次比较都要进行自动装箱,这回导致性能下降,对于数组域,则要把以上这些原则应用到每一个元素,如果数组域中的每个元素都很重要,就可以使用其中一个Arrays.equals方法。

有些对象引用域包含null可能是合法的,所以,为了避免可能导致NullPointerException异常,则使用静态方法Object.equals(Object o1,Object o2)来检查这类域的等同性。
处。

优先比较最有可能不一致的域,或者开销低的域。

在编写完equals方法之后,应该问自己三个问题:它是否对称性?它是否传递性?它是否一致性?

并且不要只是自问,还要编写单元测试来检验这些特性,除非用AutoValue(Goole开源的框架)生成equals方法,在这种情况下就可以放心的省略测试。如果答案是否定,就要找出原因,再相应的修改equals方法的代码逻辑。equals方法也必须满足其他两个特征(自反性和非空性),当然,这两种特性通常会自动满足。

除了上述的注意点之外,下面给出一些整体的告诫:

  • 1.覆盖equals方法时总是要覆盖hashCode方法
  • 2.不要企图让equals方法过于智能,如果只是简单的测试域中的值是否相等,则不难做的equals约定,如果项过度的的去寻求各种等价关系,则很容易陷入麻烦之中。把任何一种别名形式考虑到等价范围内,往往不是个好主意。例如,File类不应该试图把指向同一文件的符号链接当作相等对象来看待。所幸File类没有这样做。
  • 3.不要将equals声明中的Object对象替换为其他的类型。程序员编写出下面这样的equals方法并不鲜见,这会使得程序员花上好几个小时都搞不清楚为什么它不能正常工作。(因为重载会导致父类向下强制类型转换)

总而言之,不要轻易的覆盖equals方法,除非特殊需求,在许多情况下,从Object处继承就够了。如果要覆盖equals方法,一定要比较这个类的所有关键域,并且检查覆盖的equals方法是否遵守equals合约的所有五个条款。


参考文章:https://www.jianshu.com/p/0eb9112c6591
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值