一:怎样重写equals()方法?
重写equals()方法看起来非常简单,但是有许多改写的方式会导致错误,并且后果非常严重。要想正确改写equals()方法,你必须要遵守它的通用约定。下面是约定的内容,来自java.lang.Object的规范:
equals方法实现了等价关系(equivalence relation):
1. 自反性:对于任意的引用值x,x.equals(x)一定为true。
2. 对称性:对于任意的引用值x 和 y,当x.equals(y)返回true时,
y.equals(x)也一定返回true。
3. 传递性:对于任意的引用值x、y和z,如果x.equals(y)返回true,
并且y.equals(z)也返回true,那么x.equals(z)也一定返回true。
4. 一致性:对于任意的引用值x 和 y,如果用于equals比较的对象信息没有被修
改,多次调用x.equals(y)要么一致地返回true,要么一致地返回false。
5. 非空性:对于任意的非空引用值x,x.equals(null)一定返回false。
二:重写equals方法的要点:
1. 使用==操作符检查“实参是否为指向对象的一个引用”。
2. 使用instanceof操作符检查“实参是否为正确的类型”。
3. 把实参转换到正确的类型。
4. 对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹
配。对于既不是float也不是double类型的基本类型的域,可以使用==操作符
进行比较;对于对象引用类型的域,可以递归地调用所引用的对象的equals方法;
对于float类型的域,先使用Float.floatToIntBits转换成int类型的值,
然后使用==操作符比较int类型的值;对于double类型的域,先使用
Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较
long类型的值。
5. 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传
递的、一致的?(其他两个特性通常会自行满足)如果答案是否定的,那么请找到
这些特性未能满足的原因,再修改equals方法的代码。
三:hashCode
hashCode主要是用于散列集合,通过对象hashCode返回值来与散列中的对象进行匹配,通过hashCode来查找散列中对象的效率为O(1),如果多个对象具有相同的hashCode,那么散列数据结构在同一个hashCode位置处的元素为一个链表,需要通过遍历链表中的对象,并调用equals来查找元素。这也是为什么要求如果对象通过equals比较返回true,那么其hashCode也必定一致的原因。
为对象提供一个高效的hashCode算法是一个很困难的事情。理想的hashCode算法除了达到本文最开始提到的要求之外,还应该是为不同的对象产生不相同的hashCode值,这样在操作散列的时候就完全可以达到O(1)的查找效率,而不必去遍历链表。假设散列中的所有元素的hashCode值都相同,那么在散列中查找一个元素的效率就变成了O(N),这同链表没有了任何的区别。
hashCode()的返回值和equals()的关系如下:
- 如果x.equals(y)返回“true”,那么x和y的hashCode()必须相等。
- 如果x.equals(y)返回“false”,那么x和y的hashCode()有可能相等,也有可能不等。
四.
设计
hashCode()
[1]
把某个非零常数值,例如
17
,保存在
int
变量
result
中;
[2]
对于对象中每一个关键域
f
(指
equals
方法中考虑的每一个域):
[2.1]boolean
型,计算
(f ? 0 : 1);
[2.2]byte,char,short
型,计算
(int);
[2.3]long
型,计算
(int) (f ^ (f>>>32));
[2.4]float
型,计算
Float.floatToIntBits(
afloat
)
;
[2.5]double
型,计算
Double.doubleToLongBits(
adouble
)
得到一个
long
,再执行
[2.3];
[2.6]
对象引用,递归调用它的
hashCode
方法
;
[2.7]
数组域,对其中每个元素调用它的
hashCode
方法。
[3]
将上面计算得到的散列码保存到
int
变量
c
,然后执行
result=31*result+c;
[4]
返回
result
。
这个算法存在这么几个问题需要探讨:
1. 为什么初始值要使用非0的整数?这个的目的主要是为了减少hash冲突,考虑这么个场景,如果初始值为0,并且计算hash值的前几个域hash值计算都为0,那么这几个域就会被忽略掉,但是初始值不为0,这些域就不会被忽略掉,示例代码:
01 |
import java.io.Serializable; |
03 |
public class Test implements Serializable { |
05 |
private static final long serialVersionUID = 1L; |
07 |
private final int [] array; |
09 |
public Test( int ... a) { |
14 |
public int hashCode() { |
16 |
for ( int element : array) { |
17 |
result = 31 * result + element; |
22 |
public static void main(String[] args) { |
23 |
Test t = new Test( 0 , 0 , 0 , 0 ); |
24 |
Test t2 = new Test( 0 , 0 , 0 ); |
25 |
System.out.println(t.hashCode()); |
26 |
System.out.println(t2.hashCode()); |
如果hashCode中result的初始值为0,那么对象t和对象t2的hashCode值都会为0,尽管这两个对象不同。但如果result的值为17,那么计算hashCode的时候就不会忽略这些为0的值,最后的结果t1是15699857,t2是506447
2. 为什么每次需要使用乘法去操作result? 主要是为了使散列值依赖于域的顺序,还是上面的那个例子,Test t = new Test(1, 0)跟Test t2 = new Test(0, 1), t和t2的最终hashCode返回值是不一样的。
3. 为什么是31? 31是个神奇的数字,因为任何数n * 31就可以被JVM优化为 (n << 5) -n,移位和减法的操作效率要比乘法的操作效率高的多。