java-覆盖equals时规则不容忽视

尽管object是一个具体类,涉及它就是为了扩展它,它所有的非final方法(equals,hashCode,toString,clone和finalize)都有一些通用的规定,因为它们被设计就是用来覆盖(override)的。任何一个类,它在覆盖这些方法的时候,都有责任遵守这些约定。

本篇文章主要讲解覆盖equals方法需要遵守的规定

覆盖equals方法看起来很简单,但是有许多覆盖方法会导致错误,并且后果很严重,最容易避免这类问题的方法就是不覆盖equals方法

那么什么时候应该覆盖Object.equals呢?

如果类具有自己特有的“逻辑相等"概念(不等同对象等同概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。

在覆盖的时候,必须要遵守它的通用约定。

  1. 自反性。对于任何非null的引用值x,x.equals(x)必须返回true
  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。
  4. 一致性。对于任何非null的引用值x、y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一直返回true,或者一致返回false。
  5. 对于任何非null的引用值x,x.equals(null)必须返回false。
除非你对数学比较熟悉,不然这些规定确实有点恐惧,但是如果你不遵守它们,你会发现你的程序会变的很不正常,甚至崩溃。
第一个要求必须说明对象等于其自身。
代码简单说明
public final class TestEquals {
	private final String str;
	public TestEquals(String str){
		if(str==null){
			throw new NullPointerException();
		}
		this.str=str;
	}
	@Override
	public boolean equals(Object obj) {
		//这边只是满足了规定的第一条
		if(obj instanceof TestEquals){
			return true;
		}
		return false;
	}
}
如果违背了这一条,你用集合类(collection),把这个类的实例添加到集合中,改集合的contains将果断告诉你,该集合不包含你刚刚添加的实例。
第二个要求是任何两个对象必须要保持一致
代码简单说明
public final class TestEquals {
	private final String str;

	public TestEquals(String str) {
		if (str == null) {
			throw new NullPointerException();
		}
		this.str = str;
	}

	@Override
	public boolean equals(Object obj) {
		// 这边只是满足了规定的第一条和第二条
		return obj instanceof TestEquals
				&& ((TestEquals) obj).str.equalsIgnoreCase(str);
	}
}
如果违背了第二条,当前对象面对你的对象的时候,你完全不知道这些对象的行为时什么样的。
第三个是传递性,如果一个对象等于第二个对象,第二个对象等于第三个对象,则第一个对象一定等于第三个对象。
可能大家在做面向对象涉及的时候,继承可能也会用到。
如果父类A实现了equals方法,子类B继承父类A,如果子类B有自己的属性,如果子类B没有实现equals方法,则直接从父类A继承过来。

然而A.equals(B)返回true。B.equals(A)返回false。

如果子类B重新覆盖了equals方法。里面加上了自己属性相等的条件。然而两个不同的子类对象B1 B2和一个父类A。B1.equals(A) 和A.equals(B2)都返回true,但是B1.equals(B2)则返回false,违反了传递性 因为2个子类不同对象的数据内容肯定不一样,所以返回false。

怎么解决呢?事实上,这就是面向对象语言中关于等价关系的一个基本问题。我们无法在扩展类可实例化的同事,既增加新的值组件,同时保留equals约定。

所以采用: 复合优先继承。让要继承的类里面有父类的引用。就是把原来的继承关系打破。一个类包含另外一个类的对象。

第四点就是无论类是否可变,都不要是equals方法依靠一些可靠的资源,例如 java.net.URL中的equals方法,依赖对URL中主机的IP地址的比较。将一个主机名转换IP地址可能需要访问网络,随着时间推移,不确保会发生相同的结果。这样会导致URL的equals方法的一致性(遗憾的是,因为兼容性的要求,这一行为无法改变)。
第五点非空性,这点比较好理解,代码说明
	@Override
	public boolean equals(Object obj) {
		//满足非空性
		if(obj==null){
			return false;
	}

所以在覆盖equals方法的时候不要太智能,老老实实遵守这些规则,编写完成,应该问自己三个问题,是否对称,传递和一致。而且equals中中参数的Object对象不要替换成其他的类型。
最后一点,也是最最要的一点,覆盖equals方法总要覆盖haseCode。 在每个覆盖了equals方法的类中,则必须覆盖haseCode方法。如果不这样做,就会违反Object.haseCode方法,从而导致该类无法结合基于散列的集合一起正常工作。
java中基于散列的结合包括 HashMap,HashSet和Hashtable。
  1. 相等的对象必须返回相等的散列码。
代码简单说明:
public class TestEquals {
	private String str;

	public TestEquals(String str) {
		if (str == null) {
			throw new NullPointerException();
		}
		this.str = str;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (obj == this) {
			return true;
		}
		if (!(obj instanceof TestEquals)) {
			return false;
		}
		TestEquals te = (TestEquals) obj;
		if (te.str.equals(str)) {
			return true;
		}
		return false;
	}
}

假如你企图让这个类与HashMap一起使用:
		Map<TestEquals,String> m=new HashMap<TestEquals,String>();
		m.put(new TestEquals("13888888888"),"xiaohong");
你可能期望调用get方法,通过这个new TestEquals("13888888888")返回“xiaohong"这个字符串,但是实际是返回null,这里涉及2个TestEquals对象。第一插入map中,第二个通过对象获取名字。这两个对象相等,但是散列码不相等,违反了haseCode规定,因为你把电话号码放到一个散利桶中,却从另外一个散列桶中查找电话号码,就算在一个散列桶中,返回也是null,因为HashMap有一项优化,可以把每个项相关联的散列码缓存起来,如果散列码不相同,也不必要检验对象的等同性。
修正这个问题很简单,就是覆盖hashCode方法返回相同的散列码。
	@Override
	public int hashCode() {
		return 20;
	}
这样虽然散列码相同,因为确保了相同的对象有相同的散列码,但是效果极其恶劣,因为它使得每个对象都有相同的散列码,因此,每个对象都映射到同一个散列桶中,是散列表退化成链表,使得本线性时间运行的程序变成以平方级时间在运行,对于很大的数据量,会关系到散列表能否正常运行。
好的散列函数应该为不相等的对象产生不同的散列码
所以要覆盖hashCode方法,必须要实现把集中不相等的实例均匀分布到所有的散列值上。想达到这理想的情况非常困难。所以接近这种理想的状态,也可以实现,网上大家搜搜应该有相应的解决方法,但是。
编写散列函数式个研究课题,最后还是留个数学家和理论方面的计算机科学家来完成。






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值