我们知道,如果对象之间判断相等,需要使用equals
方法,但其实这个方法在我们创建新对象的时候就需要进行重写,当然现在IDEA或者Lombok帮我们快捷搞定了,导致不少同学不知道里面的细节,现在来讲一下。
1. equals使用的反例
先看一个反例
@AllArgsConstructor
public class MyEqual {
private int id;
private String name;
public static void main(String[] args) {
MyEqual equal1 = new MyEqual(1, "bb");
MyEqual equal2 = new MyEqual(1, "bb");
System.out.println(equal1.equals(equal2));//false
}
}
可以看到,我们的本意是为了比较下如果id和name都相同的话,比较应该返回true的,但由于没有重写,返回了false,所以重写该方法应该有哪些原则呢?
2. equals重写原则
有如下几个。
1.自反性,只要该对象不为null,则obj.equals(obj)应该返回true,自己与自己比总应该返回true吧。
2.对称性,对于任何引用x,y,当x.equals(y)返回true时,y.equals(x)也应该返回true,前后保持一致。
3.传递性,对于任何引用x,y,z,如果x.equals(y)为true,x.equals(z)为true,那么y.equals(z)也应该为true。
4.一致性,如果x,y引用的对象没有发生变化,那么多次调用x.equals(y)会有相同的结果。
5.对于任何非空引用x,x.equals(null),应始终返回false。
当然这些原则过一遍即可,重写该方法还不仅仅需要这几点。
根据这些原则,让我们来重写下equals方法吧。
3. 最佳实践
首先我们要搞清楚,equals是用在哪里的,你可能会说,这不是废话吗,当然是两个对象之间的比较咯。
好,那么我问你,相同类型的比较使用equals,没有问题,但如果父子类也想用equals来比较呢?应该考虑什么?
这。。。 是不是有点答不上来了?
所以我们分情况讨论,虽然大多数的比较都是同类之间的,但可能真有某些特殊情况呢?
步骤是这样的。
1 ) 显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量。
2 ) 检测 this 与 otherObject 是否引用同一个对象:
if (this = otherObject) return true;
这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。
3 ) 检测 otherObject 是否为 null, 如 果 为 null, 返 回 false。这项检测是很必要的。
if (otherObject = null) return false;
4 ) 比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改
变,就使用 getClass 检测:
if (getClass() != otherObject.getCIassO) return false;
这里我特意空了几行,因为这两种情况是不同的。
如果所有的子类都拥有统一的语义,就使用 instanceof 检测:
if (!(otherObject instanceof ClassName)) return false;
5 ) 将 otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject;
6) 比较otherObject与当前对象的字段是否相等。
7)重写hashcode方法,这个书中没有提及,这里做个补充,因为我们第一步比较内存地址是否相同,比较的就是hashcode,如果不进行重写,或者只是重写了部分字段,可能到会导致错误。
常规比较,一般都是类型相同的情况。
@AllArgsConstructor
public class MyEqual {
private int id;
private String name;
@Override
public boolean equals(Object otherObject) {
//1.先比较地址是否相等 这样开销最小
if (this == otherObject) return true;
//2.如果比较的对象会null 则直接返回false
if (otherObject == null) return false;
//3.检测比较的对象与当前对象是否属于同一个类 不属于则返回false
if (this.getClass() != otherObject.getClass()) return false;
//4.转化为特定类型
MyEqual myEqual = (MyEqual) otherObject;
//5.最终进行字段的比较
return myEqual.id == id && Objects.equals(myEqual.name, name);
}
@Override
public int hashCode() {
//不要忽略,如果这里返回一个常量 将导致所有比较都相等了
return Objects.hash(id, name);
}
public static void main(String[] args) {
MyEqual equal1 = new MyEqual(1, "bb");
MyEqual equal2 = new MyEqual(1, "bb");
System.out.println(equal1.equals(equal2));//true
}
}
统一语义的情况,比如父子类的id相等就代表相等,无需考虑其他情况。
@Data
public class Demo2 extends Demo {
private String name;
public static void main(String[] args) {
Demo d1 = new Demo();
d1.setId(1);
Demo d2 = new Demo();
d2.setId(1);
System.out.println(d1.equals(d2));//true
}
}
@Data
class Demo {
private int id;
@Override
public boolean equals(Object o) {
if (this == o) return true;
//如果所有子类都拥有统一的语义 即只要子类的id相等就代表相等 可以用 instanceof
if (!(o instanceof Demo)) return false;
Demo demo = (Demo) o;
return id == demo.id;
}
@Override
public int hashCode() {
//不要忽略,如果这里返回一个常量 将导致所有比较都相等了
return Objects.hash(id);
}
}
当然,说了这么多,其实我们一般都是使用自动生成的equals方法,这里推荐使用Lombok插件,非常好用,并且在存在继承关系时,如果要重写equals时,也能快速搞定。
主要介绍的是这个注解
@EqualsAndHashCode(callSuper = true)
如果callSuper为true,则代表比较是否相等时会将父类的字段也比较进去,否则就只比较子类字段。
可以看到,父子类各有一个属性,而只使用常用的@Data这样比较是会有问题的,大家可以自己去试一下。
@EqualsAndHashCode(callSuper = true)
public class Demo2 extends Demo {
private String name;
public static void main(String[] args) {
Demo2 d1 = new Demo2(1, "bb");
Demo2 d2 = new Demo2(1, "bb");
System.out.println(d1.equals(d2));//true
}
public Demo2(int id, String name) {
super(id);
this.name = name;
}
}
@Data
class Demo {
private int id;
public Demo(int id) {
this.id = id;
}
}
“一个人能做到什么,并不完全取决于血统,而是他想做到什么。我认为你不行,不是说血统或者能力,而是你没有目标,”楚子航说,“没有什么目标能让你豁出去、用尽全力,豁不出去的人是没有用的,就算你的血统比我们都强。”