Java进阶 —— 覆盖equals方法的技巧

来源:互联网 发布:必应词典mac版 编辑:程序博客网 时间:2024/05/07 18:49

本文是我在学习Effective Java这本书时的一些体会,用于总结学习,部分内容来自书上。


类Object中有equals()这个方法,该方法用于比较两个对象是否相等。

Object类中的源码如下:

public boolean equals(Object obj) {return (this == obj);}


这里Object提供的equals方法是比较两个对象的内存地址是否相等,也就是比较两个引用是否指向同一个对象。

例如:obj1.equals(obj2)为true,说明obj1和obj2指向内存里面同一个对象。

在这种情况下,类的每个实例都只与自身相等。但在现实业务中,我们常常需要比较两个对象的逻辑是否相等,这种比较我们希望它们是在逻辑上相等,而不是指向同一个对象。此时我们需要覆盖equals方法。


对于枚举类型,每个值至多只存在一个对象,即逻辑相同与对象等同是一回事,此类不需要覆盖equals方法。


equals方法实现的等价关系如下:

1.自反性,对于任何非null的引用值x,x.equals(x)必须返回true。

若覆盖时违反这一条等价关系,则把该类的实例添加到集合中,该集合的contains方法将告诉你,该集合不包含这个实例。

2.对称性,对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。

3.传递性,对任何非null的引用值x、y和z,如果x.equals(y)返回true,并y.equals(z)返回true,那么x.equals(z)也应该返回true。

下面我举一个违反这条规则的例子,首先超类Ball如下:

public class Ball {private final int size;public Ball(int size) {this.size = size;}@Overridepublic boolean equals(Object obj) {if(!(obj instanceof Ball))return false;Ball p = (Ball)obj;return (p.size == size);}}

扩展ball类,为其增加颜色属性,构造子类ColorBall:


public class ColorBall extends Ball{private final Color color;public ColorBall(int size, Color color) {super(size);this.color = color;}}
此时我们没有覆盖equals方法,这时ColorBall中的equals方法从父类Ball中继承过来,使用equals方法做比较时会忽略掉ColorBall中的color属性。这时没有违反equals的约定,但也没有达到比较两个对象是否完全等同的业务。

下面我覆盖Ball中的equals方法:

public boolean equals(Object obj) {if(!(obj instanceof ColorBall))return false;return (super.equals(obj) && ((ColorBall)obj).color == color);}

当覆盖了equals方法后,当两个ColorBall对象的大小size和颜色color相同时,才返回true。

但是新的问题出现了,倘若我用一个普通无颜色的球和一个有颜色的球比较时:

public static void main(String[] args) {Ball b = new Ball(5);ColorBall cb = new ColorBall(5,Color.RED);System.out.println(b.equals(cb)); //trueSystem.out.println(cb.equals(b)); //false}
此时普通球和有色球的比较总是为true,而有色球和普通球的比较却总是为false,这里就违反了对称性原则


下面我修改ColorBall的equals方法,让其在混合比较时候忽略掉颜色:

public boolean equals(Object obj) {if(!(obj instanceof Ball))return false;if(!(obj instanceof ColorBall))return obj.equals(this);return (super.equals(obj) && ((ColorBall)obj).color == color);}
这时比较两个球,先判断是否颜色球,如果不是则返回普通球和有色球的比较结果,如果是则返回有色球与有色球的比较结果。

这样可以解决混合比较中产生的问题,确保了 对称性,但是却牺牲了传递性

public static void main(String[] args) {ColorBall cb1 = new ColorBall(5,Color.RED);Ball b = new Ball(5);ColorBall cb2 = new ColorBall(5,Color.BLUE);System.out.println(cb1.equals(b)); //trueSystem.out.println(b.equals(cb2)); //trueSystem.out.println(cb1.equals(cb2)); //false}
根据传递性,cb1.equals(b)和b.equals(cb2)都返回true,那么cb1.equals(cb2)也应该返回true,这里却返回了false。

这是因为前两种比较属于混合比较忽略掉了颜色信息,而第三种比较考虑了颜色信息。


这了产生了一个面向对象语言中关于等价关系的一个基本问题。我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。

倘若我们在equals方法中,用getClass测试代替instanceof测试,那么我们既可以扩展实例化类和增加新组件同时保留equals的约定

public boolean equals(Object obj) {if(!(obj == null || obj.getClass() != getClass()))return false;Ball p = (Ball)obj;return (p.size == size);}
注意:这样只有当对象具有相同的实现时,才能使对象等同。

假设我要检验一个球的大小是否小于等于5且大小为整数,size<=5, size∈(1,2,3,4,5);

下面使用set实现:

private static final Set<Ball> ballSet;static {ballSet = new HashSet<Ball>();ballSet.add(new Ball(1));ballSet.add(new Ball(2));ballSet.add(new Ball(3));ballSet.add(new Ball(4));ballSet.add(new Ball(5));}public static boolean onBallSet (Ball b) {          return ballSet.contains(b);      }  


这种方法利用了Set类的contains方法进行比较,效果会很好。

但假设我不添加值组件的方式扩展Ball,例如让它的构造器纪录创建了多少个实例。

public class CounterBall extends Ball {private static final AtomicInteger counter = new AtomicInteger();public CounterBall(int size) {super(size);counter.incrementAndGet(); //ounter自增 }public int numberCreated() {return counter.get();}}

这里假设我们调用onBallSet(new CounterBall(1) ),传递一个counterBall对象,大小为1。如果Ball是使用getClass()来覆盖equals方法的话(此时要求contains()里面的对象实现必须与set中对象的实现相同才能进行比较,即所有大小小于等于5的球必须是使用Ball类实现的才能与集合中进行比较),那么无论传递的CounterBall的size值是什么,onBallSet都将返回false,因为HashSet集合利用equals方法检验包含条件时,没有找到与Ball对应的实例。

若使用instanceof覆盖equals方法,则在上述调用中能很好的达到不同实现方式的球只比较大小。

为了解决上述问题,我建议采取复合优先继承的方式,这里我的ColorBall不再继承Ball,而是在ColorBall内加入一个私有的Ball域,并提供一个getBall()的方法。

public class ColorBall{ //这里不再继承Ball类private final Ball ball;private final Color color;public ColorBall(int size, Color color) {if(color != null) throw new NullPointerException();ball = new Ball(size);this.color = color;}public Ball getBall() {return ball;}@Overridepublic boolean equals(Object obj) {if(!(obj instanceof ColorBall))return false;ColorBall cb = (ColorBall)obj;return cb.getBall().equals(ball) && cb.color.equals(color);}}


注意:你可以在抽象类的子类增加新的值组件,同时不违反equals的约定。只要不可能直接创建超类的实例,前面所述的问题都不会发生。


4.一致性,对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就将一致地返回true或者一致地返回false。

当你在写一个类时,需要考虑它是否应该是不可变的,如果认为它是不可变的,就必须保证equals方法满足:相等的对象永远相等,不相等的对象永远不相等。

5.非空性,对于任何非null的引用值x,x.equals(null)必须返回false。

下面总结一下,高质量覆盖equals方法的诀窍;

1.使用==操作符检查“参数是否为这个对象的引用”,若是则返回true。

2.使用instanceof操作符检查“参数是否为正确的类型”,如果不是则返回false。

3.把参数转换成正确的类型,因为前面使用了instanceof测试,所以这一步不会出错。

4.对于该类中的每个关键域,检查参数中的域是否与该对象中对应的域相匹配。

对于既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归调用equals方法;对于float域,可以使用Float.compare方法;对于double域,可以使用Double.compare方法。

5.当你写完equals方法时,应该检查是否对称的、传递的、一致的。


最后,覆盖equals时总要覆盖hashCode。

不要将equals声明中的Object对象替换为其他的类型。例如:

public boolean equals (MyClass o) {//错误写法}

这样并没有覆盖Objet.equals的方法,而是重载了这个方法,也就是在原来有的equals方法上,再添加一个强制类型的equals方法,这样容易使程序出错。

为防止这种错误,建议在覆盖equals方法时,加上@Override这个注解,它会在编译的时候告诉你哪里出错。


以上,便是我的学习心得,若有不正确的地方,欢迎指出。

渡边渔夫

1 0