《Effective Java》阅读笔记8 覆盖equals方法请遵守通用约定

113 阅读10分钟

1.序

equals是Obejct提供的通用方法,作用是比较两个实例逻辑上值是否相等,在自己设计的类中要考虑是否需要复写equlas方法,以及遵守equals方法的规范约定。    重写equals方法看起来似乎很简单,但是有许多重写方式会导致错误,而且后果非常严重。

2.不需要覆盖equals的情景

  • 类的每个实例本质上都是唯一的。对于==代表活动实体而不是值(value)的类==来说确实如此,例如Thread。Object提供的equals实现对于这些类来说正是正确的行为。
  • 不关心类是否提供了“逻辑相等(logical equality)”的测试功能。例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数序列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了。
  • ==超类已经覆盖了equals==,从超类继承过来的行为对于子类也是适合的。例如大多数的Set实现都从AbstractSet继承equals实现,List实现从AbstractList继承equals实现,Map实现从AbstractMap继承equals实现。
  • ==类是私有的或是包级私有的==,可以确定它的equals方法永远不会被调用。在这种情况下,无疑是应该覆盖equals方法的,一方它被意外调用:
@override
public boolean equals(Object obj){
    throw new AssertionError();
}

3.需要覆盖equals的情景

如果==类具有自己特有的“逻辑相等”概念(不同于对象等同的概念)==,==而且超类还没有覆盖equals以实现期望的行为==,这是我们就需要覆盖equals方法。这通常属于”值类(value class)”的情形。

值类==仅仅是一个表示值的类==,例如Integer或者Date等。我们在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向了同一个对象。在这种情况下需要覆盖equals方法。

public class IntObject {

	private int i ;

	public IntObject(int i) {
		super();
		this.i = i;
	} @Override
	public boolean equals(Object obj) { if(obj instanceof IntObject){ IntObject p = (IntObject) obj; if(this.i == p.i){ return true; } }
		return false;
	} public static void main(String[] args) {
		IntObject p1 = new IntObject(1);
		IntObject p2 = new IntObject(1); System.out.println(p1.equals(p2));
		System.out.println(p1 == p2);
	}
	
}

如果上述类不提供equals方法,比较规则默认就是比较对象的引用(和 == 一样),一次只有提供了equals方法才能达到你想要的值比较。这里再补充一点:集合Set只能存放不同的元素,这个不同也是建立在equals不相等上,如果相等,则Set判定为相同的元素。

4.覆盖equals方法时的通用约定

在覆盖equals方法的时候,你必须要遵守它的通用约定。下面是约定的内容,来自Object的规范【JavaSE6】:

4.1自反性(reflexivity)

如果两个对象的引用是一样的(也即是==比较是相等的),则equals一定要相等。

4.2对称性(symmetry)

对于任何非null的引用值x和y,当且仅当y.equals(x)返回时,x.equals(y)必须返回true。

NotString.java
package com.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));
    }
}

结果

true

false

4.3传递性(transitivity

对于任何非null的引用值x,y,z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)也是true。

==无意识地违反这条规则的情形也不难想象。考虑子类的情形,它将一个新的值组件(value component)添加到了超类中。换句话说,子类增加的信息会影响到equals的比较结果==。

Point类
package com.linjie.a;

/**
 * @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;


/**
  * @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));
    }
}

结果

true

false

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

注意:我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。

在Java平台类库中,有一些类扩展了可实例化的类,并添加了新的值组件。例如java.sql.Timestamp对java.util.Date进行了扩展,并增加了nanoseconds域。Timestamp的equals实现确实违反了对称性,如果Timestamp和Date对象被用于同一个集合中,或者以其他方式被混合在一起,则会引起不正确的行为。

==Timestamp类有一个免责声明==,告诫程序员不要混合使用Date和Timestamp对象。只要你不要把他们混合在一起,就不会有麻烦,除此之外没有其他的措施可以防止你这么做,而且结果导致的错误将很难调试。Timestamp类的这种行为是个错误,不值得效仿。 注意,你可以在一个抽象(abstract)类的子类中增加新的值组件,而不会违反equals约定。

4.4一致性(consistency

equals约定的第四个要求是,如果两个对象相等,他们就必须始终保持相等,除非他们中有一个对象(或者两个都)被修改了。换句话说,==可变对象在不同的时候可以与不同的对象相等,而不可变对象则不会这样==。当你在写一个类的时候,应该仔细考虑他 ==是否应该是不可变的== 。如果认为他应该是不可变的,就必须保证equals方法满足这样的限制条件:相等的对象永远相等,不相等的对象永远不相等

无论类是否是不可变的,都不要使equals方法依赖不可靠的资源。如果违反了这条禁令,要想满足一致性的要求就十分困难了。例如,java.net.URL的equals方法依赖于对URL中主机IP地址的比较。将一个主机名转变成IP地址可能需要访问网络,随着时间的推移,不确保会产生相同的结果。这样会导致URL的equals方法违反equals约定,在时间中有可能引发一些问题。(遗憾的是,因为兼容性的要求,这一行为无法被改变。)除了极少数的例外情况,equals方法都应该对驻留在内存中的对象执行确定型的计算。

4.5非空性(Non-nullity)

==所有的对象都必须不等于null==。尽管很难想象什么情况下o.equals(null)调用会意外地返回true,但是意外抛出NullPointerException异常的情形却不难想象。通过约定不允许抛出NullPointerException异常。

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

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

MyType.java
package com.linjie.aa;


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

结果

如果漏掉了instanceof检查,并且传递给equals方法的参数又是错误类型,那么equals方法将会抛出ClassCastException异常,这就违反了equals的约定。

但是,如果instanceof的第一个操作数为null,那么,不管第二个操作数是什么类型,instanceof操作符都指定应该返回false,因此不需要单独的null检查,而应该用instanceof。

5.equals的正确使用方法

  • 1、使用操作符检查“==参数是否为这个对象的引用==”。如果是则返回true。这是一种性能优化 if(this==obj) return true;
  • 2、使用instanceof操作符检查“==参数是否为正确的类型==”,如果不是则返回false。所谓的正确的类型是指equals方法所在的那个类,或者是该类的父类或接口
  • 3、把参数转化成正确的类型:因为上一步已经做过instanceof测试,所以确保转化会成功
  • 4、对于该类的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配(其实就是比较两个对象的值是否相等了)
  • 5、当你编写完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;
    }
}

6.特别注意

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

7.参考文献

codeleading.com/article/738…

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

7.你们要的免费书来了