四时宝库

程序员的知识宝库

Effective java覆盖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));
}
}
结果


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.yy;
    }
    }
    }
    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§);
}
}
结果


从结果很明显可以看出前一种忽略了颜色信息所以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§);
System.out.println(“cp是对象,cp2是参数”);
System.out.println(cp.equals(cp2));
}
}
结果
如果没有第三方cp2的时候,则是正确

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

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

我们无法在扩展可实例化类的同时,既增加新的值组件,同时又保留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));
}
}
结果

如果漏掉了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可以避免这种错误发生

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接