Java覆盖equals时请遵守通用约定
覆盖equals时请遵守通用约定
1、为什么要覆盖equals
因为默认equals在比较两个对象时,是看他们是否指向同一个地址
。但有时,我们需要两个不同对象只要是某些属性相同就认为它们equals()的结果为true。比如:
class Person{
String name;
public Person(String name){
this.name = name;
}
}
Person aperson = new Person("a")
Person bperson = new Person("a")
我们希望的结果是aperson 等于 bperson
.
解决方法
1. 使用 ==
java中 a == b
判断a 和 b 是否引用同一个object, 所以aperson == bperson
会返回false, 这种方法不行
2. 覆盖euqals()
因为java中的所有class 都直接或间接地继承自 Object 类, 而Object类有一些基本的方法如 equals
(), toString
()等等……我们就可以覆盖其中的equals方法, 然后调用aperson.equals(bperson)
来判断两者是否相等.
我的第一次实现如下
class Person{
String name;
public Person(String name){
this.name = name;
}
@Override
public boolean equals(Object o){
return this.name == o.name;
}
}
但是第8行return this.name == o.name
; 处报错了 name can’t be resolved or not a field
于是我又换了种写法:
class Person{
String name;
public Person(String name){
this.name = name;
}
@Override
public boolean equals(Person o){
return this.name == o.name;
}
}
然而这次第七行public boolean equals(Person o)
处又报错了The method equals(Person) of type Person must override or implement a supertype method
这两个错误的原理我不是很清楚, 等之后我弄清楚之后会更新上来
于是我只好再改个方法如下
class Person{
String name;
public Person(String name){
this.name = name;
}
@Override
public boolean equals(Object o){
return this.name == o;
}
}
这次倒是没有报错了, 不过调用的时候非常不美观, 得写成aperson.equals(bperson.name)
最终版本如下:
class Person{
String name;
public Person(String name){
this.name = name;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(!(o instanceof Person)) {
return false;
}
Person person = (Person) o;
return this.name.equals(person.name);
}
}
终于aperson.equals(bperson)
可以返回true
了
2、需要覆盖equals方法的时机
如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时候我们就需要覆盖equals方法。
2.1、不需要覆盖equals方法的情况
(1)类的每个实例本质上都是唯一的。
对于代表活动实体而不是值(value)的类来说,例如Thread。Object提供的equals实现对于这些类来说是正确的行为。
(2)不关心类是否提供了“逻辑相等(logical equality)”的测试功能。
例如,java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机数列,但是设计者并不认为客户需要或者期望这样的功能。在这样的情况下,从Object继承的equals实现就已经足够了。
(3)超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
(4)类是私有的或者包级私有的,可以确定它的equals方法永远不会被调用。
在这种情况下,无疑是应该覆盖equals的,以防止它被意外调用。这种情况下,只是对equals的一种废弃,并没有加什么新的功能。
@Override
public boolean equals(Object obj) {
throw new AssertionError(); //Method is never called
}
2.2、需要覆盖equals方法的情况
那么,什么时候应该覆盖equals方法呢?
如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals方法,这通常属于值类的情形。
值类仅仅是一个表示值的类,例如:Integer或者String,程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是像了解它们是否指向同一个对象
,不仅必须覆盖equals方法,而且这样做也使得这个类的实例可以被用作映射表(map)的键(key)或者集合(set)的元素,使映射或者集合表现出逾期的行为。
有一种"值类"不需要覆盖equals方法,即用实例受控确保"每个值最多只存在一个对象"的类,枚举类型就属于这种类,对于这样的类而言,逻辑相同与对象相同是一回事。
在覆盖equals方法的时候,必须遵守通用约定,下面是约定的内容,来自Object的规范:
- 1、
自反性
:对于任何非null的引用值x,x.equals(x)
,必须返回true。 - 2、
对称性
:对于任何非null的引用值x、y,当且仅当x.equals(y)
返回true时,y.equals(x)
也必须返回true。 - 3、
传递性
:对于任何非null的引用值x、y、z,如果x.equals(y)
返回true,y.equals(z)
也返回true,那么x.equals(z)也必须返回true。 - 4、
一致性
:对于任何非null引用值x、y,只要equals的比较操作在对象中所用的信息没有修改,多次调用x.equals(y)
就会一致的返回true,或者一致的返回false。 - 5、
非空性
:对于任何非null引用值x,x.equals(null)必须返false
。
2.2.1、自反性
自反性说明一个对象是等于其自身, 自己和自己相等, Object的也就实现了这个了:
publicbooleanequals(Object obj){
return (this == obj);
}
2.2.2、对称性
简单来说, 就是a=b
成立的话, 那么b=a
必定成立.
来看个例子, 有一个CaseInsensitiveString
类, 这个类比较的时候会忽略大小写:
final class CaseInsensitiveString{
private final String s;
public CaseInsensitiveString(String s) throws NullPointerException{
if (s == null) {
throw new NullPointerException();
}
this.s = s;
}
@Override
public boolean equals(Object o) {
if(o instanceof CaseInsensitiveString) {
return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
}
if(o instanceof String) {
return s.equalsIgnoreCase((String) o);
}
return false;
}
}
覆盖了equals, 判断s与CaseInsensitiveString类的s或者和String忽略大小后是否相等, 否则就返回false, 然后我们来测试一下
CaseInsensitiveString cis1 = new CaseInsensitiveString("boom!");
CaseInsensitiveString cis2 = new CaseInsensitiveString("Boom!");
String s = "BoOM!";
System.out.println(cis1.equals(cis2));
System.out.println(cis1.equals(s));
输出正如我们所料, 都是true, 但是别忘了自反性啊, cis1.equals(s)输出true, s.equals(cis1)也应该是输出true的, 事实上s.equals(cis1)的结果是false
. 显然违反了对称性, String类的equals并不知道不区分大小写的CaseInsensitiveString类, 因此s.equals(cis1)返回了false.
为了解决这个问题, 只要将企图与String互操作的那段代码从equals中删除就行了
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveString
&& ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
2.2.3、传递性
最复杂的就是传递性了, 离散中最麻烦的也是求传递闭包了.
传递性的意思也很简单的, 就是a=b, b=c的话, 那么a和c也是相等的.
有一个Point类, 用来表示坐标点
class Point{
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
Point p = (Point)o;
return x == p.x && y == p.y;
}
}
然后又有一个ColorPoint类, 来表示带颜色的点
class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
没有复写equals的情况下, 虽然ColorPoint和Point作比较的时候能饭后正确的结果, 但是两个ColorPoint之间做比较的时候忽略了颜色信息
, 这显然不是我们想要的结果, 于是乎:
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
这样对了吗? 抱歉, 问题还是很大, 虽然实现了两个有色点之间的比较, 但是当普通点和有色点比较的时候, 违反了对称性, 普通点和有色点比较会忽略颜色, 而有色点和普通点则总是返回false.
继续改:
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
if(!(o instanceof ColorPoint)) {
return o.equals(this);
}
return super.equals(o) && ((ColorPoint) o).color == color;
}
如果o不是ColorPoint, 就用o去比较this, 这样就会忽略颜色信息了, 测试一下:
ColorPoint p1 = new ColorPoint(1, 1, Color.RED);
Point p2 = new Point(1, 1);
ColorPoint p3 = new ColorPoint(1, 1, Color.GREEN);
System.out.println(p1.equals(p2));
System.out.println(p2.equals(p3));
返回的都是true, 很好, 对称性的问题解决了, 等等, 这里不是在讨论传递性吗!!!
按照传递性来说, p1=p2, p2=p3, 所以p1和p3肯定是相等啊, 但是这里很明显就是不相等的, 大家又不是色盲.
事实上,这是面向对象语言中关于等价关系的一个基本问题。我们无法在拓展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。
你可能听说,在equals方法中庸getClass()
测试代替instanceof
测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) {
return false;
}
Point p = (Point)o;
return x == p.x && y == p.y;
}
只有当对象相同时才 比较, 这样虽然解决了问题, 但是却不是我们想要的解决方法.
来看一个更好的实现, 用复合代替继承:
上述问题根据该计划我们不在让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公有视图(view)方法,此方法返回一个与该有色点处在相同位置的普通Point对象,最后代码是:
class ColorPoint {
private final Color color;
private final Point point;
public ColorPoint(int x, int y, Color color) {
if (color == null) {
throw new NullPointerException();
}
point = new Point(x, y);
this.color = color;
}
public Point asPoint() {
return point;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
注意:你可以在一个抽象(abstract)类的子类中增加新的值组件,而不会违反equals约定。
2.2.4、一致性
如果两个对象是相等的, 就应该保持一直是相等的, 除非这两个对象中有一个或者两个都被修改了, 所以记住: 相等的对象永远相等, 不相等的对象永远不相等.
2.2.5、非空性
所有的对象都不能为null, 尽管很难想象什么情况下o.equals(null)会返回true. 但是意外抛出NullPointerException
异常的可能却不难想象,
所以可以这样写来不允许抛出NullPointerException
异常
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
}
3、高质量equals方法的几个注意点
1.使用 == 操作符检查”参数是否为这个对象的引用。
如果是,则返回true
,这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
2.使用 instanceof 操作符检查”参数是否为正确的类型。
如果不是,则返回false
。一般来说,所谓“正确的类型”是指equals方法所在的那个类。某些情况下,是指该类所实现的某个接口。如果类实现的接口进行了equals约定,允许在实现了该接口的类之间进行比较,那就使用接口,集合接口如Set,List,Map和Map.Entry具有这样的特性。
3.把参数转换正确的类型。
- 因为转换之前进行过
instanceof
测试,所以确保会成功。
4.对于该类的每个关键域,检查参数中的域是否与该对象中的对应的域相匹配。
如果这些测试全部成功,则返回true,否则返回false
,如果第2步中的类型是个接口,就必须接口方法访问参数中的域,如果该类型是一个类,也许就能够直接访问参数中的域,这要取决与它们的可访问性。
当你编写完equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的
?当然equals也必须满足其他两个约定(自反性、非空性)但是这两种约定通常会自动满足
3.1、规范案例
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;
}
}
4、除了上述的注意点之外,下面给出一些整体的告诫:
- 1、覆盖
equals
方法时总是要覆盖hashCode
方法 - 2、
不要企图让equals方法过于智能
,如果只是简单的测试域中的值是否相等,则不难做的equals约定,如果项过度的的去寻求各种等价关系,则很容易陷入麻烦之中。- 把任何一种别名形式考虑到等价范围内,往往不是个好主意。例如,File类不应该试图把指向同一文件的符号链接当作相等对象来看待。所幸File类没有这样做。
- 3、
不要将equals声明中的Object对象替换为其他的类型
。程序员编写出下面这样的equals方法并不鲜见,这会使得程序员花上好几个小时都搞不清楚为什么它不能正常工作。(因为重载会导致父类向下强制类型转换)
public boolean equals(MyClass obj) {
...
}
上述代码,使用了具体的类MyClass作为参数,这会导致错误。原因在于,这个方法并没有重写(override)Object.equals方法,而是重载(overload)了它。某些情况下,这个具体化的equals方法会提高一些性能,但这样极有可能造成不易察觉的错误。
4.1、==和equals()的区别
1、= =:是运算符
。 既可以比较基本类型
也可以比较引用类型
。对于基本类型就是比较值
,而引用类型就是比较内存地址
。并且必须保证符号左右两边的变量类型一致,即都是比较基本类型,或者都是比较引用类型。
2、equals()
:是java.lang.Object
类里面的方法。只能适用于引用数据类型
。如果类的该方法没有被重写过默认也是= =
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Customer() {
super();
}
public Customer(String name, int age) {
super();
this.name = name;
this.age = age;
}
//自动生成的equals()
@Override
public boolean equals(Object obj) { // 用传进来的对象做比较
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Customer other = (Customer) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Customer [name=" + name + ", age=" + age + "]";
}
}
public static void main(String[] args) {
Customer cust1 = new Customer("Tom",21);
Customer cust2 = new Customer("Tom",21);
System.out.println(cust1.equals(cust2)); //true(因为没有重写就是false,因为调用的是Object父类的equals,这比较的是 == ,是地址值,重写了equals方法才是对比里面的具体内容)。
}
5、总结
参考
1、如何正确的覆盖equals和hashCode
2、覆盖equals方法需要注意的
3、Effective Java 【对于所有对象都通用的方法】第10条 覆盖equals方法请遵守通用规范