反反复复看了几遍,感觉第八条写的真心好,虽然平时知道怎么重写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));
}
}
结果
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));
}
}
结果
从结果很明显可以看出前一种忽略了颜色信息所以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的时候,则是正确
但是有了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可以避免这种错误发生