本文是我在学习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这个注解,它会在编译的时候告诉你哪里出错。


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

渡边渔夫