1、何时需要重写equals
相信javaer们应该都知道equals方法,它是基类大佬Object中的一个方法,所以java下面所有的类都“自带”这个方法。看方法名就知道,意图就是对比传入的目标对象, 跟自己是否“相等”。我们先看看这个方法在Object类中的实现:
public boolean equals(Object obj) {
return (this == obj);
}
这个实现也算简单粗暴了,直接用“==”来跟目标对象作比较,这个意图就是:除非对方就是是自己本身,否则就不相等。 但就是因为这样的粗暴,造成限定得太死了。实际开发中,可能原生的equals不能满足业务的需求。所以需要重写,例如Integer中重写的equals方法,可以看到Integer并不强制目标对象是“自己本体”,而只是对比了被自己包装的int基本类型数值,这样的实现更加符合实际业务要求:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();//对比被包装的整型数值
}
return false;
}
关于何时才需要重写equals方法,《Effective Java》中提到:
如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖equals。这通常属于“值类”的情形。值类仅仅是一个表示值得类,例如Integer和String。程序员在利用equals方法来比较值对象的引用时,希望知道他们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。
这句话翻译得有点拗口,不过意思还是明确的,何时才是需要重写equals,可以总结为以下两点:
- **该类只代表一个具体的“值“。**例如Double、Integer、Long等这种包装类型,其实这些类再怎么复杂,它们也只是代表一个数值,只要类型和数值相等,它们对象也理应是相等的。
- **业务逻辑相同的对象。**这个可以看作是第一点的延申,即对象间的成员变量、行为表现是一致的时候,也应该理解成是相同的对象。例如一个商品类Goods,只要它的商品ID,商品名称等关键信息是相等的,就是同一件商品。
2、重写equals方法要遵守的约定
2.1 重写equals错误示范
重写equals方法看似很简单,但是有很多重写的方式会导致意想不到的错误。举一个jdk里的一个错误例子,java.sql.Timestamp是继承自java.util.Date的,这两个类都分别重写了equals方法。我们可以来看看:
java.util.Date.equals:
public boolean equals(Object obj) {
return obj instanceof Date && getTime() == ((Date) obj).getTime();
}
java.sql.Timestamp:
public boolean equals(java.lang.Object ts) {
if (ts instanceof Timestamp) {
return this.equals((Timestamp)ts);
} else {
return false;
}
}
这两个类的equals方法,如果是各自单独使用起来的话,是没有任何问题的,但如果Date和Timestamp这两个类一起使用的话就有问题了,例如把这两个类的对象都存到一个集合list里,就会出问题了:
static List<java.util.Date> dateList = new ArrayList<>();
public static void addTimeObj(java.util.Date date) {
if(!dateList.contains(date)) {
dateList.add(date);
}
}
public static void main(String[] args) {
long currentTimeMillis = 1586669016742L;
java.sql.Timestamp timestamp = new Timestamp(currentTimeMillis);
Date date = new java.util.Date(currentTimeMillis);
System.out.println(date.equals(timestamp));//true
System.out.println(timestamp.equals(date));//false
addTimeObj(timestamp);
addTimeObj(date);//date对象加不进去
}
由上面的代码可以看出,date.equals(timestamp)是true,timestamp.equals(date)是false,这个违反了下面将要说的对称性 ,因此有可能会产生意想不到的后果,例如第二个addTimeObj方法的调用,date是加不进去的,因为ArrayList.contains利用了对象的equals方法来进行对比的,然而timestamp.equals(date)==false。
public boolean contains(Object o){
//...省略一些代码
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))//timestamp.equals(date)==false
return i;
return false;
//...省略一些代码
}
2.2 重写equals要遵守的”军规“
所以,重写equals时,是有一定的约束的,《Effective Java》中提到了一些重写必须遵循的”军规“:
- 自反性:对于任何非null的引用值x,x.equals(x)必须返回true;
- 对称性:对于任何非null的引用x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true;
- 传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,则x.equals(z)返回true;
- 一致性:对于任何非null的引用值x和y,只要equals的比较操作对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false;
- 对于任何非null的引用x,x.equals(null)必须返回false。
以上几条规定,如果违反了其中一条,就有可能会产生意想不到的后果,程序也许会表现得不正常,甚至崩溃。就像上面java.util.Date和java.sql.Timestamp的例子,这样的bug是很难排查出来的。
3、如何正确快速的重写equals方法
我们得知重写equals方法的一些约束后,会觉得,实现一个equals方法怎么那么难?其实,要实现equals方法,并不难,在实际开发中,有以下两个快速稳当的方法。
3.1 利用IDE的自动生成equals
目前的IDE都有equals方法的快捷生成方法,比如eclipse,”Alt+Shift+S“组合键—>Generate hashCode() and equals(),即可同时生成hashCode方法和equals方法*(由此也可以看出equals方法和hashCode方法是捆绑一起的,实现equals方法,必须也得实现hashCode方法)*
public class Graph {
int n;
LinkedList<Integer> [] table;
//....此处省略了hashCode方法的实现
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Graph other = (Graph) obj;
if (n != other.n)
return false;
if (!Arrays.equals(table, other.table))
return false;
return true;
}
}
上面的equals方法是eclipse生成的(篇幅原因省略了hashCode的代码),粗略的看起来还挺全的,其实不一定符合我们实际开发中的业务需求,而且略显臃肿。所以在平时开发中用IDE生成的这种代码,往往都是需要调整一下,以适应实际业务,当然,前提是要遵循上面说到的几个军规。
3.2 重写equals方法的几个诀窍(步骤)
IDE生成equals方法非常便捷,但是生成的代码有时是比较”鸡肋“。因此,我们是否可以在遵循军规的基础上,自己快速手写一个equals方法呢?当然是可以的,可以按照以下几个步骤来一步步实现。
- 使用**==**操作符检查参数是否为这个对象的引用。如果是,则返回true。
- 使用instanceof操作符检查参数是否是正确的类型。如果不是,返回false。
- 把参数转成正确的类型。因为已经经过第二步的ininstanceof 校验,所以类型会转换成功。
- 对于该类中每个”关键域“,检查参数中的域是否与该对象中的对应的域相匹配。在检查域的时候,应该先检查性能开销最低的域,或者逻辑最可能不一致的域,按照这样的有序安排可以尽可能减少比较的次数,从而提高性能。
- 编写玩equals之后,应该考虑并检验三个问题:它是否是对称的、传递的、一致的?
根据这五个步骤,写出来的equals才是安全可靠的,下面构造了一个Goods类,并实现了它的equals方法和hashCode方法,最后再来检测下它的对称性、传递性、一致性,都是可以的!
public class Goods {
int id;
String goodsName;
@Override
public boolean equals(Object obj) {
//使用==操作符检查参数是否为这个对象的引用。如果是,则返回true。
if(this == obj)
return true;
//使用instanceof操作符检查参数是否是正确的类型。如果不是,返回false。
if(!(obj instanceof Goods))
return false;
//把参数转成正确的类型。
Goods target = (Goods)obj;
/*
* 对于该类中每个”关键域“,检查参数中的域是否与该对象中的对应的域相匹配。
* 在检查域的时候,应该先检查性能开销最低的域,或者逻辑最可能不一致的域,
* 按照这样的有序安排可以尽可能减少比较的次数,从而提高性能。
*/
if(this.id != target.id)
return false;
if(target.goodsName == null || !target.goodsName.equals(this.goodsName))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((goodsName == null) ? 0 : goodsName.hashCode());
result = prime * result + id;
return result;
}
}
4、写在最后
覆盖equals方法是要比较严谨的,如果不是迫不得已,就不要轻易去重写equals方法。如果要重写,则一定要比较这个类的所有关键域,并且查看它们是否遵循equals的几条军规!而且还有另外一个很重要的是,重写equals方法后,必须同步实现hashCode方法,否则就会违反了hashCode的通用约定,而关于hashCode 如何重写,请看姐妹篇《如何实现高效的hashCode方法》
equals方法,你今天掌握了吗?