本文是我在学习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;
}
@Override
public 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)); //true
System.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)); //true
System.out.println(b.equals(cb2)); //true
System.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;
}
@Override
public 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这个注解,它会在编译的时候告诉你哪里出错。
以上,便是我的学习心得,若有不正确的地方,欢迎指出。
渡边渔夫