effective java(9) 之覆盖equals时总要覆盖hashCode

effective java 之覆盖equals时总要覆盖hashCode


1、每个覆盖了equals方法的类中,也必须覆盖hashCode方法。 


2、为什么覆盖equals时,总要覆盖hashCode?
原因是,根据Object规范:
如果不这样的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。


在引用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一的返回同一个整数。
在一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。 


如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。 


如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要蚕声不同的整数结果。
但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。(比如,当你一个entity只根据id比较是否相等,但是在没实例化之前,没有id数值,那么默认的equals返回false,但是hashCode返回的值却相等。) 


hashCode如何生成?
也就是,两个相等的对象,必须要有相等的散列码(hashCode)。
两个对象,进行比较的时候,使用的关键域是一样的,然后,使用这些关键域作为参数来生成散列码。
两个对象相应的关键域的值相等,那么,使用相等的关键域,使用同样的生成方法,那么,生成出来的散列码是相等的。


3、两个对象的hashCode相等,那么这两个对象是相等的么?
不一定。因为,equals的实现,有可能是使用其它方式,未必只是简单地比较hashCode方法中用到的关键域。
所以,不能根据对象的hashCode来判定两个对象是否相等。


4、两个对象根据equals(Object)方法进行比较,它们不相等,那么,这两个对象的hasoCde一定是不同的么?
不一定。同样的道理,因为equals方法的实现,未必只是简单地比较hashCode方法中用到的关键域的值。


5、一个例子:
	import java.util.HashMap;
	import java.util.Map;


	public final class PhoneNumber {
		private final short areaCode;
		private final short prefix;
		private final short lineNumber;


		public PhoneNumber(int areaCode, int prefix, int 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 o) {
			if (o == this)
				return true;
			if (!(o instanceof PhoneNumber))
				return false;
			PhoneNumber pn = (PhoneNumber) o;
			return pn.lineNumber == lineNumber && pn.prefix == prefix
					&& pn.areaCode == areaCode;
		}
		// Broken - no hashCode method!
		// A decent hashCode method - Page 48
		// @Override public int hashCode() {
		// int result = 17;
		// result = 31 * result + areaCode;
		// result = 31 * result + prefix;
		// result = 31 * result + lineNumber;
		// return result;
		// }


		// Lazily initialized, cached hashCode - Page 49
		// private volatile int hashCode; // (See Item 71)
		//
		// @Override public int hashCode() {
		// int result = hashCode;
		// if (result == 0) {
		// result = 17;
		// result = 31 * result + areaCode;
		// result = 31 * result + prefix;
		// result = 31 * result + lineNumber;
		// hashCode = result;
		// }
		// return result;
		// }


		public static void main(String[] args) {
			Map m = new HashMap();
			m.put(new PhoneNumber(707, 867, 5309), "Jenny");


			System.out.println(new PhoneNumber(707, 867, 5309)
					.equals(new PhoneNumber(707, 867, 5309)));
			
			System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
		}
	}

结果返回:true null
原因:在覆盖equals方法的时候,没有覆盖hashCode方法。PhoneNumber作为HashMap的K,在main方法中,生成了两个PhoneNumber实例,这两个PhoneNumber实例通过equals方法比较是相等的。
但是,PhoneNumber,并没有覆盖hashCode方法,所以,这两个实例具备不同的hashCode。现在要做的就是,覆盖hashCode方法,然后,使通过equals比较相等的两个PhoneNumber实例返回相等的hashCode。
一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”。 


6、优化
import java.util.HashMap;
	import java.util.Map;


	public final class PhoneNumber {
		private final short areaCode;
		private final short prefix;
		private final short lineNumber;


		public PhoneNumber(int areaCode, int prefix, int 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 o) {
			if (o == this)
				return true;
			if (!(o instanceof PhoneNumber))
				return false;
			PhoneNumber pn = (PhoneNumber) o;
			return pn.lineNumber == lineNumber && pn.prefix == prefix
					&& pn.areaCode == areaCode;
		}
		/*@Override
		public int hashCode() {
			return new HashCodeBuilder(17, 37).append(lineNumber).append(prefix)
					.append(areaCode).toHashCode();
		}*/
		// Broken - no hashCode method!
		// A decent hashCode method - Page 48
		//相等的实例生成相等的散列码,不相等的pn分散到不同的散列桶中
		@Override
		public int hashCode() {
			int result = 17;
			result = 31 * result + areaCode;
			result = 31 * result + prefix;
			result = 31 * result + lineNumber;
			return result;
		}
		// Lazily initialized, cached hashCode - Page 49
		// private volatile int hashCode; // (See Item 71)
		/*延迟初始化
		@Override
		public int hashCode() {
			int result = hashCode;
			if (result == 0) {
				result = 17;
				result = 31 * result + areaCode;
				result = 31 * result + prefix;
				result = 31 * result + lineNumber;
				hashCode = result;
			}
			return result;
		}*/
		public static void main(String[] args) {
			Map m = new HashMap();
			m.put(new PhoneNumber(707, 867, 5309), "Jenny");


			System.out.println(new PhoneNumber(707, 867, 5309)
					.equals(new PhoneNumber(707, 867, 5309)));
			
			System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
		}
	}
结果:true Jenny

hashCode主要确定关键域,关键域指的是覆盖的equals方法中涉及到的每个域;然后,使用这些关键域,作为参数,用它们生成一个hashCode。


7、实现hashCode,把集合中不想等的实例均匀分布到所有的可能的散列值上的解决方案:


1.把某个非零的常数值保存在一个名为result的int类型变量中。


2.对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤: 
a.为该域计算int类型的散列码c 
i.boolean – f?1:0; 
ii.byte char short int – int(f); 
iii.long – int(f^f>>>32); 
iv.Float – Float.floatToIntBits(f); 
v.double – Double.DoubleToLongBits(f); -> 2.a.iii 
vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals方式来比较这个域 
,则同样为这个域递归的调用hashCode.如果需要更复杂的比较,则为这个域计算一个”范式” 
(canonical representation),然后针对这个范式调用hashCode.如果这个域的值为null,则返回0 
(或者其他某个常数,但通常是0) 。
vii.如果该域是一个数组,则要把每一个元素当做单独的域来处理,也就是说,递归的应用上述规则,对每个 
重要的元素计算散列码,然后根据步骤2.b的方法把这些散列值组合起来.如果数组域中的每个元素都很重要 
,可以利用Arrays.hashCode方法 。
b.按照 result = 31 * result + c 把步骤2.a中计算得到的散列码c合并到result中 。
为什么要用31:他是一个奇素数.如果乘法为偶数,且乘法溢出,信息就会丢失,因为与2相乘等价于位移运算 。


3.返回result 。


4.相等的实例是否具有相等的散列码。


8、优化建议

如果一个类是不可变的,并且计算散列码的开销比较大。
应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。
如果你觉得这种类型的大多数对象会被用作散列键,就应该在创建实例的时候重新计算散列码.
否则,可以选择 延迟初始化 散列码 一直到hashCode被第一次调用的时候才初始化。

参考文献:http://www.cnblogs.com/ttylinux/

每天努力一点,每天都在进步。

你可能感兴趣的:(Effective,java读书笔记)