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.参考文献
https://codeleading.com/article/73873153239/
本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!
1.计算机网络----三次握手四次挥手
2.梦想成真-----项目自我介绍
3.你们要的设计模式来了
4.一字一句教你面试“个人简介”
5.接近30场面试分享
6.你们要的免费书来了