`
moisen
  • 浏览: 4743 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

转载:对几个通用的Java hashCode重写方案的一些思考和探讨

    博客分类:
  • Java
阅读更多

在我们刚开始学习Java的时候就被教导,在编写类的时候,如果覆盖了Object的equals方法,那么必须要覆盖hashCode方法,并且如果两个对象用equals方法比较返回true,那么这两个对象hashCode返回的值也必须是相等的,并且对于同一个对象,equals方法需要比较的属性值没有被修改,那么每次调用hashCode返回的值应该是一致的。

hashCode主要是用于散列集合,通过对象hashCode返回值来与散列中的对象进行匹配,通过hashCode来查找散列中对象的效率为O(1),如果多个对象具有相同的hashCode,那么散列数据结构在同一个hashCode位置处的元素为一个链表,需要通过遍历链表中的对象,并调用equals来查找元素。这也是为什么要求如果对象通过equals比较返回true,那么其hashCode也必定一致的原因。

为对象提供一个高效的hashCode算法是一个很困难的事情。理想的hashCode算法除了达到本文最开始提到的要求之外,还应该是为不同的对象产生不相同的hashCode值,这样在操作散列的时候就完全可以达到O(1)的查找效率,而不必去遍历链表。假设散列中的所有元素的hashCode值都相同,那么在散列中查找一个元素的效率就变成了O(N),这同链表没有了任何的区别。

这种理想的hashCode算法,如果是为具体业务的对象去设计应该不是很难,比如很多的数据库映射对象都存在一个整形的id属性,这个id属性往往在整个系统中是唯一的,那么hashCode在重写的时候返回这个id的值就可以了,equals比较的时候也是去比较id的值,并且对象在从数据库初始化之后是不可变的,这样就完全达到了理想的情况。这些对象保存在散列中,查找效率会是完全的O(1),不需要遍历任何链表。

本文着重讨论的是通用的hashCode实现,所谓的通用就是适合Java中每一个对象的hashCode算法实现。每个类的结构不尽相同,要想产生一个适合所有场景的理想hashCode算法几乎是不可能的,要设计通用的hashCode算法,我们只能去不断接近理想的情况。下面是几种实现方式。

《Effective Java》中推荐的实现方式

Google首席Java架构师Joshua Bloch在他的著作《Effective Java》中提出了一种简单通用的hashCode算法
1. 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
2. 选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:
  (1) 如果是boolean值,则计算f ? 1:0
  (2) 如果是byte\char\short\int,则计算(int)f
  (3) 如果是long值,则计算(int)(f ^ (f >>> 32))
  (4) 如果是float值,则计算Float.floatToIntBits(f)
  (5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
  (6) 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。  否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0
  (7) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去    重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
  java.util.Arrays.hashCode(long[])的具体实现:
public static int hashCode(long a[]) {
        if (a == null)
            return 0;
        int result = 1;
        for (long element : a) {
            int elementHash = (int)(element ^ (element >>> 32));
            result = 31 * result + elementHash;
        }
        return result;
}
 Arrays.hashCode(...)只会计算一维数组元素的hashCOde,如果是多维数组,那么需要递归进行hashCode的计算,那么就需要使用Arrays.deepHashCode(Object[])方法。
 
3. 最后,要如同上面的代码,把每个域的散列码合并到result当中:result = 31 * result + elementHash;
4. 测试,hashCode方法是否符合文章开头说的基本原则,这些基本原则虽然不能保证性能,但是可以保证不出错。
 
这个算法存在这么几个问题需要探讨:
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,移位和减法的操作效率要比乘法的操作效率高的多。
 
另外如果对象是不可变的,那么还推荐使用缓存的方式,在对象中使用一个单独的属性来存储hashCode的值,这样对于这个对象来说hashCode只需要计算一次就可以了。
private volatile int hashCode = 0;

@Override
public int hashCode() {

    int result = hashCode;
    if(result == 0) {

        ...//计算过程
    }
    return result;
}
 注意,缓存属性必须是volatile的,这样可以在并发访问环境中保持内存可见性。否则会产生线程安全问题。
 
此外上面所提到的基本元素类型的hashCode计算,其算法同JDK中其包装器类型所覆盖的hashCode逻辑一致
java.lang.Long的hashCode实现:
/**
     * Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Long}. The result is
     * the exclusive OR of the two halves of the primitive
     * {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  long} value held by this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Long}
     * object. That is, the hashcode is the value of the expression:
     *
     * <blockquote>
     *  {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  (int)(this.longValue()^(this.longValue()>>>32))}
     * </blockquote>
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        return (int)(value ^ (value >>> 32));
    }
 java.lang.Float的hashCode实现:
/**
  * Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Float} object. The
  * result is the integer bit representation, exactly as produced
  * by the method {<a href="http://my.oschina.net/link1212" target="_blank" rel="nofollow">@link</a>  #floatToIntBits(float)}, of the primitive
  * {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  float} value represented by this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Float}
  * object.
  *
  * @return a hash code value for this object.
  */
 public int hashCode() {
     return floatToIntBits(value);
 }
 java.lang.double的hashCode实现:
/**
  * Returns a hash code for this {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Double} object. The
  * result is the exclusive OR of the two halves of the
  * {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  long} integer bit representation, exactly as
  * produced by the method {<a href="http://my.oschina.net/link1212" target="_blank" rel="nofollow">@link</a>  #doubleToLongBits(double)}, of
  * the primitive {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  double} value represented by this
  * {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  Double} object. That is, the hash code is the value
  * of the expression:
  *
  * <blockquote>
  *  {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  (int)(v^(v>>>32))}
  * </blockquote>
  *
  * where {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  v} is defined by:
  *
  * <blockquote>
  *  {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  long v = Double.doubleToLongBits(this.doubleValue());}
  * </blockquote>
  *
  * @return  a {<a href="http://my.oschina.net/codeo" target="_blank" rel="nofollow">@code</a>  hash code} value for this object.
  */
 public int hashCode() {
     long bits = doubleToLongBits(value);
     return (int)(bits ^ (bits >>> 32));
 }
 org.apache.commons.lang.builder.HashCodeBuilder的实现
org.apache.commons.lang.builder.HashCodeBuilder能够通过反射机制来自动计算对象的hashCode值。其核心方法是静态方法:reflectionHashCode,这个方法有多个重载的版本,这些重载版本最终都是调用的 int reflectionHashCode( intinitialNonZeroOddNumber, int multiplierNonZeroOddNumber, Object object,

            boolean testTransients, Class reflectUpToClass, String[] excludeFields)

其余的版本只是提供不同的默认参数,从而简化了构建的过程。比如:

 

public static int reflectionHashCode(Object object) {
        return reflectionHashCode(17, 37, object, false, null, null);
}

 然后构建的过程是这样的,除了指定过滤的,比如transient属性、excludeFields指定的属性之外,会遍历其它的类属性,然后通过反射的方式获取属性值,如果属性是数组,则会遍历数组,否则会调用属性的hashCode, 如果是多维数组,会去递归取hashCode值,对单个属性计算hash值的代码如下:

 

 

public HashCodeBuilder append(Object object) {
        if (object == null) {
            iTotal = iTotal * iConstant;

        } else {
            if(object.getClass().isArray()) {
                // 'Switch' on type of array, to dispatch to the correct handler
                // This handles multi dimensional arrays
                if (object instanceof long[]) {
                    append((long[]) object);
                } else if (object instanceof int[]) {
                    append((int[]) object);
                } else if (object instanceof short[]) {
                    append((short[]) object);
                } else if (object instanceof char[]) {
                    append((char[]) object);
                } else if (object instanceof byte[]) {
                    append((byte[]) object);
                } else if (object instanceof double[]) {
                    append((double[]) object);
                } else if (object instanceof float[]) {
                    append((float[]) object);
                } else if (object instanceof boolean[]) {
                    append((boolean[]) object);
                } else {
                    // Not an array of primitives
                    append((Object[]) object);
                }
            } else {
                iTotal = iTotal * iConstant + object.hashCode();
            }
        }
        return this;
    }

 这里要小心,因为它是直接调用属性对象的hashCode,如果是基本类型,那么就会调用包装器中提供的hashCode方法,如果是引用类型,那么需要仔细检查引用类型的hashCode方法,以免产生违反hashCode基本原则的情况。

 

 

然后剩下的计算过程,同Effective Java中描述的基本类似,不过这里的hash初值和乘数因子可以自己来设置,默认的情况是使用了17 和 37两个互质数。

HashCodeBuilder最大好处是使用方便,一行代码就能搞定hashCode的重写问题,并且让代码很清晰,但是它有这么几个值得注意的地方:

1. 使用反射会对程序的性能造成影响,不过Java反射机制为了把性能影响降到最低,对类似getFields()之类的操作都采用了Cache策略,对一般的程序而言,这些性能开销往往可以忽略不计。另外如果使用的是不可变对象,那么强烈建议把hashCode Cache住,这样可以极大的提高hashCode计算的性能。

2. 因为默认会处理所有的field(除了transient修饰的field),所以一定要测试是否违反hashCode的基本原则(为了保障基本原则的正确,建议跟org.apache.commons.lang.EqualsBuilder搭配使用),尤其是当类的域中包含引用类型的时候,一定要递归检查引用类型的hashCode.

链式的HashCodeBuilder

最后结合Effective Java中提供的算法编写一个链式的HashCode生成器:
import java.util.Arrays;

/**
 * 一个链式调用的通用hashCode生成器
 * 
 * <a href="http://my.oschina.net/arthor" target="_blank" rel="nofollow">@author</a>  hongze.chi@gmail.com
 * 
 */
public final class HashCodeHelper {

    private static final int multiplierNum = 31;

    private int hashCode;

    private HashCodeHelper() {
        this(17);
    }

    private HashCodeHelper(int hashCode) {
        this.hashCode = hashCode;
    }

    public static HashCodeHelper getInstance() {
        return new HashCodeHelper();
    }

    public static HashCodeHelper getInstance(int hashCode) {
        return new HashCodeHelper(hashCode);
    }

    public HashCodeHelper appendByte(byte val) {
        return appendInt(val);
    }

    public HashCodeHelper appendShort(short val) {
        return appendInt(val);
    }

    public HashCodeHelper appendChar(char val) {
        return appendInt(val);
    }

    public HashCodeHelper appendLong(long val) {
        return appendInt((int) (val ^ (val >>> 32)));
    }

    public HashCodeHelper appendFloat(float val) {
        return appendInt(Float.floatToIntBits(val));
    }

    public HashCodeHelper appendDouble(double val) {
        return appendLong(Double.doubleToLongBits(val));
    }

    public HashCodeHelper appendBoolean(boolean val) {
        return appendInt(val ? 1 : 0);
    }

    public HashCodeHelper appendObj(Object... val) {
        return appendInt(Arrays.deepHashCode(val));
    }

    public HashCodeHelper appendInt(int val) {
        hashCode = hashCode * multiplierNum + val;
        return this;
    }

    public int getHashCode() {
        return this.hashCode;
    }
}
 通过这种链式调用的方式,没有反射的开销,另外可以比较方便的选择要参与计算的属性,代码也比较清晰,但是如果参与计算的属性值过多,那么会造成调用链过长的情况。为保持代码的整洁,也可以分多个链来调用。示例代码:
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() {
        HashCodeHelper hashCodeHelper = HashCodeHelper.getInstance();
        hashCodeHelper.appendInt(array[0]).appendInt(array[1]).appendInt(array[2]);
        hashCodeHelper.appendInt(array[3]).appendInt(array[4]).appendInt(array[5]);
        return hashCodeHelper.getHashCode();
    }
}
 转载自:http://my.oschina.net/chihz/blog/56256
分享到:
评论

相关推荐

    Java_重写equals()和hashCode()

    这篇博客将深入探讨这两个方法的重写规则和应用场景。 首先,`equals()` 方法是Object类中的一个基础方法,用于比较两个对象是否相等。默认情况下,它比较的是对象的内存地址,也就是引用是否相同。但在实际开发中...

    Java重写equals同时需要重写hashCode的代码说明

    Java重写equals同时需要重写hashCode的代码说明,以及如何重写hashCode方法,此代码演示按照effective java书籍说明的重写思路。代码中演示了使用集合存储对象,并且对象作为key,需重写equals和hashCode.

    重写equals和hashcode方法_equals_重写equals和hashcode方法_

    在Java编程语言中,`equals()` 和 `hashCode()` 方法是Object类中的两个核心方法,所有类都默认继承自Object类。这两个方法在处理对象比较和集合操作时起着至关重要的作用。当我们创建自定义类并需要对对象进行精确...

    java 序列化和重写 hashCode 的原因

    在Java编程语言中,序列化(Serialization)和重写`hashCode`及`equals`方法是两个重要的概念,它们各自有着特定的用途,并且在某些情况下相互关联。下面将详细阐述这两个概念及其应用。 首先,Java序列化是将一个...

    java中hashcode()和equals()的详解

    在Java编程语言中,`hashCode()`和`equals()`方法是对象身份验证的关键组成部分,它们主要用于对象的比较和哈希表(如HashMap、HashSet等)的操作。理解这两个方法的工作原理对于编写高效和可靠的代码至关重要。 ...

    关于hashCode()和equals()的本质区别和联系

    本文将详细介绍 hashCode() 和 equals() 的本质区别和联系,并探讨在创建 Java 类时如何定义这些方法。 hashCode() 方法 hashCode() 方法是 Object 类中的一个方法,它返回对象的哈希码值。哈希码值是一个整数,它...

    java中hashcode和equals的详解.pdf

    本文详细介绍了 Java 中的 hashCode 和 equals 方法,探讨了这两个方法的作用、实现机制和使用场景。通过对 hashCode 和 equals 方法的深入分析,我们可以更好地理解 Java 集合的实现原理和哈希表的工作机制。 一、...

    Java重写equals及hashcode方法流程解析

    Java中的equals和hashCode方法是两个非常重要的方法,它们都是Object类中的方法。在实际开发中,正确地重写这两个方法对于确保程序的正确性和性能至关重要。下面,我们将详细地介绍Java重写equals及hashCode方法的...

    Java基础加强_ArrayList_HashSet的比较及Hashcode分析

    在Java编程语言中,ArrayList和HashSet是两种常用的集合类,它们各自有其特性和应用场景。在实际开发中,理解它们的差异以及如何有效地利用它们是非常重要的。本篇将深入探讨ArrayList与HashSet的区别,并分析...

    java中Hashcode的作用.docx

    1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。 2. 如果两个对象根据equals(Object o)方法是相等的,...

    equals与hashCode在实际开发中的重写写法

    在Java编程语言中,`equals()` 和 `hashCode()` 方法是两个非常重要的成员,尤其是在处理对象比较和集合操作时。这两个方法通常与`Object`类中的默认实现相关联,但为了在实际开发中实现正确的对象比较和哈希表操作...

    深入 HashCode 方法~

    - 对于不同的类,默认情况下,`hashCode()` 方法的实现可能会有所不同,但通常会依赖于对象的一些关键属性来生成一个整数值作为 `HashCode`。 2. **自定义类中的 HashCode 实现**: - 当我们自定义一个类时,通常...

    解析Java对象的equals()和hashCode()的使用

    在Java语言中,equals()和hashCode()两个函数的使用是紧密配合的,你要是自己设计其中一个,就要设计另外一个。在多数情况下,这两个函数是不用考虑的,直接使用它们的默认设计就可以了。但是在一些情况下,这两个...

    javahashcode()和equals()和==的介绍和区别.pdf

    在Java编程中,`hashCode()`、`equals()`以及`==`是三个经常被提及的概念,它们在处理对象的比较和存储时起着关键作用。本文将深入探讨这三个概念的介绍、区别以及它们在Java对象比较中的应用。 首先,`hashCode()`...

    Java中equals,hashcode和==的区别

    Java 中 equals、hashcode 和==的区别是 Java 编程语言中一个经常遇到的问题。这三个概念都是用来比较对象的,但是它们之间存在着本质的区别。 首先,==号是Java中的一个运算符,用于比较两个对象的内存地址是否...

    JAVA_高级特性(hashCode,clone,比较器,Class反射,序列化)

    ### Java 高级特性详解 #### 一、`hashCode` ...正确地重写 `equals` 和 `hashCode` 方法、使用 `Comparator` 进行排序、利用反射机制和序列化技术,以及实现 `clone` 方法都是开发高质量 Java 应用程序的重要技能。

    Java理论与实践:hashCode()和equals()方法

    本文介绍了Java语言不直接支持关联数组,可以使用任何对象作为一个索引的数组,但在根Object类中使用 hashCode()方法明确表示期望广泛使用HashMap。理想情况下基于散列的容器提供有效插入和有效检索;直接在对象模式...

Global site tag (gtag.js) - Google Analytics