疯狂java


您现在的位置: 疯狂软件 >> 新闻资讯 >> 正文

Java 重写Object类中equals和hashCode方法


 

       一:怎样重写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,这些域就不会被忽略掉,示例代码:

  import java.io.Serializable;

  public class Test implements Serializable {

  private static final long serialVersionUID = 1L;

  private final int[] array;

  public Test(int... a) {

  array = a;

  }

  @Override

  public int hashCode() {

  int result = 0; //注意,此处初始值为0

  for (int element : array) {

  result = 31 * result + element;

  }

  return result;

  }

  public static void main(String[] args) {

  Test t = new Test(0, 0, 0, 0);

  Test t2 = new Test(0, 0, 0);

  System.out.println(t.hashCode());

  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,移位和减法的操作效率要比乘法的操作效率高的多。