【Effective Java笔记】第8条:覆盖equals时请遵守通用约定

时间:2021-10-07 16:15:28

反反复复看了几遍,感觉第八条写的真心好,虽然平时知道怎么重写equals吧,但根本不会去想这么多,各种固定思维。不过现在对equals的重写又有新的认识了,下面就写关于第八条覆盖equals时请遵守通用约定的阅读笔记吧

该篇博客主要阐述

1、不需要覆盖equals的情景
2、需要覆盖equals的情景
3、对5条通用约定的理解
4、实现高质量equals的诀窍
5、注意点

一、不需要覆盖equals的情景

1、类的每个实例本质上都是唯一的

对于代表活动实体而不是值(value)的类来说确实如此,比如Thread

2、不关心类是否提供了“逻辑相等”的测试功能

书本上的例子是java.util.Random覆盖equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户需要或者期望这样的功能。在这种情况下,从Object继承得到的equals实现已经足够了

3、父类已经覆盖了equals,从父类继承过来的行为对于子类也是合适的

例如大多数的Set实现都是从AbstractSet继承equals实现

4、类是私有的或者是包级私有的,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,以防它被以外调用

这种情况下,只是对equals的一种废弃,并没有新加什么功能

@override
public boolean equals(Object obj){
    throw new AssertionError();
}

二、、需要覆盖equals的情景

如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且父类还没有覆盖equals以实现期望行为,这时就需要覆盖equals方法。覆盖equals方法的时候,必须遵守以下通用约定

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

三、对5条通用约定的理解

1、自反性

该约定仅仅说明对象必须等于其自身,此特性通常会自动满足

2、对称性

任何两个对象对于“它们是否相等”的问题都必须保持一致。比如下面就是不一致,违反了对称性的情况,NotSring类中的equals方法是忽略大小写的,只要值相等即返回true。但是String类中的equals并不会忽略大小写,即使String类本身已经重写了equals,只要值相等就返回true,但这里恰恰是大小写不同值相同的一种方式,所以就使得s.equals(ns)返回false了

NotString.java
package com.linjie;

/**
 * @author LinJie
 * @Description:这是一个非String类,作为待会与String类的一个比较
 */
public class NotString {
    private final String s;

    public NotString(String s) {
        if(s==null)
            throw new NullPointerException();
        this.s=s;
    }

    @Override
    public boolean equals(Object obj) {
        //如果equals中的实参是属于NotString,只要NotString的成员变量s的值与obj的值相等即返回true(不考虑大小写)
        if(obj instanceof NotString)
            return s.equalsIgnoreCase(((NotString) obj).s);
        //如果equals中的实参是属于String,只要NotString的成员变量s的值与obj的值相等即返回true(不考虑大小写)
        if(obj instanceof String)
            return s.equalsIgnoreCase((String) obj);
        return false;
    }
}
测试类
package com.linjie;

import org.junit.Test;

public class EqualsTest {
    @Test
    public void Test() {
        NotString ns = new NotString("LINJIE");
        String s = "linjie";
        System.out.println("NotString作为对象,String作为参数");
        System.out.println(ns.equals(s));
        System.out.println("String作为对象,NotString作为参数");
        System.out.println(s.equals(ns));
    }
}
结果

【Effective Java笔记】第8条:覆盖equals时请遵守通用约定


3、传递性

如果一个对象等于第二个对象并且第二个对象等于第三个对象,则第一个对象一定等于第三个对象。但有时也会违反这一点。比如下面的Point和其子类ColorPoint,在子类新加一个颜色特性,就会很容易违反这条约定

Point类
package com.linjie.a;

/**
 * @author LinJie
 * @Description:是两个整数型的父类
 */
public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        super();
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Point)) 
            return false;
        else {
            Point p = (Point)obj;
            return p.x==x&&p.y==y;
        }
    }
}
ColorPoint类
package com.linjie.a;


/**
 * @author LinJie
 * @Description:在父类Point基础上添加了颜色信息
 */
public class ColorPoint extends Point {
    private final String color;

    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }


    //违背对称性
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof ColorPoint))
            return false;
        else
            return super.equals(obj)&&((ColorPoint)obj).color==color;
    }
}
测试类
package com.linjie.a;

import org.junit.Test;

public class equalsTest2 {
    @Test
    public void Test() {
        Point p = new Point(1, 2);
        ColorPoint cp = new ColorPoint(1, 2, "red");
        ColorPoint cp2 = new ColorPoint(1, 2, "blue");
        System.out.println("p是对象,cp是参数");
        System.out.println(p.equals(cp));
        System.out.println("cp是对象,p是参数");
        System.out.println(cp.equals(p));
    }
}
结果

【Effective Java笔记】第8条:覆盖equals时请遵守通用约定

从结果很明显可以看出前一种忽略了颜色信息所以true,而后一种则总是false,因为参数类型不正确。导致违背了对称性


还有一种方法是保证了对称性,但却违背了传递性,修改ColorPoint类

package com.linjie.a;


/**
 * @author LinJie
 * @Description:在父类Point基础上添加了颜色信息
 */
public class ColorPoint extends Point {
    private final String color;

    public ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    //违反了传递性
    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Point))
            return false;
        if(!(obj instanceof ColorPoint))
            return obj.equals(this);//反转成Point的equals,忽略了颜色信息
        return super.equals(obj)&&((ColorPoint)obj).color==color;
    }
}
测试类
package com.linjie.a;

import org.junit.Test;

public class equalsTest2 {
    @Test
    public void Test() {
        Point p = new Point(1, 2);
        ColorPoint cp = new ColorPoint(1, 2, "red");
        ColorPoint cp2 = new ColorPoint(1, 2, "blue");
        System.out.println("p是对象,cp是参数");
        System.out.println(p.equals(cp));
        System.out.println("cp是对象,p是参数");
        System.out.println(cp.equals(p));
        System.out.println("cp是对象,cp2是参数");
        System.out.println(cp.equals(cp2));
    }
}
结果

如果没有第三方cp2的时候,则是正确

【Effective Java笔记】第8条:覆盖equals时请遵守通用约定

但是有了cp2,则违背传递性就显而易见了

【Effective Java笔记】第8条:覆盖equals时请遵守通用约定

很明显,可以看出传递性又不行咯


我们无法在扩展可实例化类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的优势即组合优先于继承的方法实现,但本篇博客就不再阐述了。


4、一致性

如果两个对象相等,它们就必须保持相等,除非它们中有一个对象(或者两个都)被修改了

5、非空性

意思是指所有的对象必须不等于null

@override
public boolean equals(Object obj){
    if(obj == null)
        return false;
    ....
}

以上if测试是不必要的。为了测试其参数的等同性,equals方法必须先把参数转化成适当的类型,以便可以调用它的方法或成员变量。在进行转化之前,equals必须使用instanceof操作符,检查其参数是否是该类的对象或子类对象

MyType.java
package com.linjie.aa;

/**
 * @author LinJie
 * 
 */
public class MyType {
    private final String s;

    public MyType(String s) {
        super();
        this.s = s;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof MyType))
            return false;
        //只要判断obj属于MyType的对象或子类对象,就可以将obj转化成MyType类型,来调用其私有成员变量
        MyType mt = (MyType)obj;
        return mt.s==s;
    }
}
测试类
package com.linjie.aa;

import java.awt.Color;

import org.junit.Test;

public class equalsTest222 {
    @Test
    public void Test() {
        MyType mt = new MyType("linjie");
        System.out.println(mt.equals(null));
    }
}
结果

【Effective Java笔记】第8条:覆盖equals时请遵守通用约定

如果漏掉了instanceof检查,并且传递给equals方法的参数又是错误类型,那么equals方法将会抛出ClassCastException异常,这就违反了equals的约定。但是,如果instanceof的第一个操作数为null,那么,不管第二个操作数是什么类型,instanceof操作符都指定应该返回false,因此不需要单独的null检查,而应该用instanceof


四、实现高质量equals的诀窍

1、使用==操作符检查“参数是否为这个对象的引用”。如果是则返回true。这是一种性能优化
if(this==obj)
    return true;


2、使用instanceof操作符检查“参数是否为正确的类型”,如果不是则返回false。所谓的正确的类型是指equals方法所在的那个类,或者是该类的父类或接口


3、把参数转化成正确的类型:因为上一步已经做过instanceof测试,所以确保转化会成功


4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)


5、当你编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?当然equals也必须满足其他两个约定(自反性、非空性)但是这两种约定通常会自动满足

根据以上的诀窍,就可以构建出一个比较不错的equals方法实现了

可以看到下面重写的equals就是根据以上几点诀窍来写的

package linjie.com.xxx;

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(short areaCode, short prefix, short lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode = (short)areaCode;
        this.prefix = (short)prefix;
        this.lineNumber = (short)lineNumber;
    }

    private static void rangeCheck(int arg,int max,String name) {
        if(arg < 0 || arg > max)
            throw new IllegalArgumentException(name +": "+ arg);
    }

    @Override
    public boolean equals(Object obj) {
        //1、参数是否为这个对象的引用
        if(obj == this)
            return true;
        //2、使用instanceof检查
        if(!(obj instanceof PhoneNumber))
            return false;
        //3、把参数转化成正确的类型
        PhoneNumber pn = (PhoneNumber)obj;
        //4、比较两个对象的值是否相等
        return pn.lineNumber == lineNumber
            && pn.prefix == prefix
            && pn.areaCode == areaCode;
    }
}

五、注意点

  • 覆盖equals时总要覆盖hashCode(见第9条,这里暂不阐述了)
  • 不要企图让equals方法过于智能:不要想过度地去寻求各种等价关系,否则容易陷入各种麻烦
  • 不要将equals声明中的Object对象替换为其他类型,不然就不是重写equals了,而是重载了。加上@override可以避免这种错误发生