hashCode()方法的性能优化

原文链接译文链接,原文作者: Robert Nystrom,译者:有孚

本文主要讨论下不同的hashCode()的实现对应用程序的性能影响。

hashCode()方法的一个主要作用就是使得对象能够成为哈希表的key或者散列集的成员。但同时这个对象还得实现equals(Object)方法,它和hashCode()的实现必须是一致的:

  • 如果a.equals(b)那么a.hashCode == b.hashCode()
  • 如果hashCode()在同一个对象上被调用两次,它应该返回的是同一个值,这表明这个对象没有被修改过。

hashCode的性能

从性能的角度来看的话,hashCode()方法的主要目标就是尽量使得不同的对象拥有不同的哈希值。JDK中所有基于哈希的集合都是将值存储在数组中的。查找元素的时候,会使用哈希值来计算出在数组中的初始查找位置;然后再调用equals()方法将给定的值和数组中存储对象的值进行比较。如果所有元素的哈希值都不一样,这会减少哈希的碰撞概率。换句话说,如果所有的值的哈希码都一样的话,hashmap(或者hashset)会蜕化成一个列表,操作的时间复杂度会变成O(n2)。

更多细节可以看下hash map碰撞的解决方案。JDK用了一个叫开放寻址的方法,不过还有一种方法叫拉链法。所有哈希码一样的值都存储在一个链表里(说反了吧)。

我们来看下不同质量的哈希值有什么区别。我们将一个正常的String和它的包装类进行比较,这个包装类重写了hashCode()方法,所有对象都返回同一个哈希值。

private static class SlowString
{
    public final String m_str;

    public SlowString( final String str ) {
        this.m_str = str;
    }

    @Override
    public int hash Code() {
        return 37;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final SlowString that = ( SlowString ) o;
        return !(m_str != null ? !m_str.equals(that.m_str) : that.m_str != null);
    }
}

下面是一个测试方法。后面我们还会再用到它,所以这里还是简单介绍一下 。它接收一个对象列表,然后对列表中的每个元素依次调用Map.put(), Map.containsKey()方法。

private static void testMapSpeed( final List lst, final String name )
{
    final Map<Object, Object> map = new HashMap<Object, Object>( lst.size() );
    int cnt = 0;
    final long start = System.currentTimeMillis();
    for ( final Object obj : lst )
    {
        map.put( obj, obj );
        if ( map.containsKey( obj ) )
            ++cnt;
    }
    final long time = System.currentTimeMillis() - start;
    System.out.println( "Time for "  + name + " is " + time / 1000.0 + " sec, cnt = " + cnt );
}

String和SlowString对象都是按照”ABCD”+i的格式生成的。处理100000个String对象需要0.041秒,而处理SlowString对象则需要82.5秒。

结果表明,String类的hashCode()方法明显胜出。我们再做另一个测试。先创建一个字符串列表,前半部分的格式是”ABCdef*&”+i,后半部分的是”ABCdef*&”+i+”ghi”(确保字符串的中间部分变化而结尾不变,不会影响哈希值的质量)。我们会创建1百万,5百万,1千万,2千万个字符串,来看下有多少字符串是共享哈希值的,同一个哈希值又会被多少个字符串共享。下面是测试的结果:

Number of duplicate hash Codes for 1000000 strings = 0

Number of duplicate hash Codes for 5000000 strings = 196
Number of hash Code duplicates = 2 count = 196

Number of duplicate hash Codes for 10000000 strings = 1914
Number of hash Code duplicates = 2 count = 1914

Number of duplicate hash Codes for 20000000 strings = 17103
Number of hash Code duplicates = 2 count = 17103

可以看到,共用同一个哈希值的字符串很少,而一个哈希值被两个以上的字符串共享的概率则非常小。当然了,你的测试数据可能不太一样——如果用这个测试程序测试你给定的字符串的话。

自动生成long字段的hashCode()方法

许多IDE生成long类型的hashcode()的方式非常值得一提。下面是一个生成的hashCode()方法,这个类有两个long类型的字段。

Number of duplicate hash Codes for 1000000 strings = 0

Number of duplicate hash Codes for 5000000 strings = 196
Number of hash Code duplicates = 2 count = 196

Number of duplicate hash Codes for 10000000 strings = 1914
Number of hash Code duplicates = 2 count = 1914

Number of duplicate hash Codes for 20000000 strings = 17103
Number of hash Code duplicates = 2 count = 17103

下面给只有两个int类型的类生成的方法:

public int hash Code() {
int result = val1;
result = 31 * result + val2;
return result;
}

可以看到,long类型的处理是不一样的。java.util.Arrays.hashCode(long a[])用的也是同样的方法。事实上,如果你将long类型的高32位和低32位拆开当成int处理的话,生成的hashCode的分布会好很多。下面是两个long字段的类的改进后的hasCode方法(注意,这个方法运行起来比原来的方法要慢,不过新的hashCode的质量会高很多,这样的话hash集合的执行效率会得到提高,虽然hashCode本身变慢了)。

public int hash Code() {
    int result = (int) val1;
    result = 31 * result + (int) (val1 >>> 32);
    result = 31 * result + (int) val2;
    return 31 * result + (int) (val2 >>> 32);
}

下面是testMapSpeed 方法分别测试10M个这三种对象的结果。它们都是用同样的值进行初始化的。

Two longs with original hashCode Two longs with modified hashCode Two ints
2.596 sec 1.435 sec 0.737 sec

可以看到,更新后的hashCode方法的效果是不太一样的。虽然不是很明显,但是对性能要求很高的地方可以考虑一下它。

高质量的String.hashCode()能做些什么

假设我们有一个map,它是由String标识符来指向某些值。map的key(String标识符)不会在内存的别的地方存储(某一时间可能有一小部分值是存储在别的地方)。假设我们已经收集到了map的所有记录,比如说在某个两阶段算法中的第一个阶段。下一步我们要通过key来查找map中的值。我们只会用map里存在的key进行查找。

我们如何能提升map的性能?前面你已经看到了,String.hashCode()返回的几乎都是不同的值,我们可以扫描所有的key,计算出它们的哈希值,找出那些不唯一的哈希值:

Map<Integer, Integer> cnt = new HashMap<Integer, Integer>( max );
for ( final String s : dict.keySet() )
{
    final int hash = s.hash Code();
    final Integer count = cnt.get( hash );
    if ( count != null )
        cnt.put( hash, count + 1 );
    else
        cnt.put( hash, 1 );
}

//keep only not unique hash codes
final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 );
for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() )
{
    if ( entry.getValue() > 1 )
        mult.put( entry.getKey(), entry.getValue() );
}

现在我们可以创建两个新的map。为了简单点,假设map里存的值就是Object。在这里,我们创建了Map<Integer, Object> 和Map<String, Object>(生产环境推荐使用TIntObjectHashMap)两个map。第一个map存的是那些唯一的hashcode以及对应的值,而第二个,存的是那些hashCode不唯一的字符串以及它们相应的值。

final Map<Integer, Object> unique = new HashMap<Integer, Object>( 1000 );
final Map<String, Object> not_unique = new HashMap<String, Object>( 1000 );

//dict - original map
for ( final Map.Entry<String, Object> entry : dict.entrySet() )
{
    final int hash Code = entry.getKey().hash Code();
    if ( mult.containsKey( hash Code ) )
        not_unique.put( entry.getKey(), entry.getValue() );
    else
        unique.put( hash Code, entry.getValue() );
}

//keep only not unique hash codes
final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 );
for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() )
{
    if ( entry.getValue() > 1 )
        mult.put( entry.getKey(), entry.getValue() );
}

现在,为了查找某个值,我们得先查找第一个hashcode唯一的map,如果没找到,再查找第二个不唯一的map:

public Object get( final String key )
{
final int hash Code = key.hash Code();
Object value = m_unique.get( hash Code );
if ( value == null )
value = m_not_unique.get( key );
return value;
}

在一些不太常见的情况下,你的这个不唯一的map里的对象可能会很多。碰到这种情况的话,先尝试用java.util.zip.CRC32或者是java.util.zip.Adler32来替换掉hashCode()的实现(Adler32比CRC32要快,不过它的分布较差些)。如果实在不行,再尝试用两个不同的函数来计算哈希值:低32位和高32位分别用不同的函数生成。hash函数就用Object.hashCode, java.util.zip.CRC32或者java.util.zip.Adler32。

(译注:这么做的好处就是压缩了map的存储空间,比如你有一个map,它的KEY存100万个字符串的话,压缩了之后就只剩下long类型以及很少的字符串了)

set的压缩效果更明显

前面那个例子中,我们讨论了如何去除map中的key值。事实上,优化set的话效果会更加明显。set大概会有这么两个使用场景:一个是将原始的set拆分成多个子set,然后依次查询标识符是否属于某个子set;还有就是是作为一个拼写检查器(spellchecker )——有些要查询的值是预想不到的值(比如拼写错误了),而就算出了些错误的话影响也不是很大(如果碰巧另一个单词也有同样的hashCode,你会认为这个单词是拼写正确的)。这两种场景set都非常适用。

如果我们延用前面的方法的话,我们会得到一个唯一的hashcode组成的Set,以及不唯一的hashCode组成的一个Set。这里至少能优化掉不少字符串存储的空间。

如果我们可以把哈希值的取值限制在一定的区间内(比如说2^20),那么我们可以用一个BitSet来代替Set,这个在BitSet一文中已经提到了。一般来说如果我们提前知道原始set的大小的话,哈希值的范围是有足够的优化空间的。

下一步就是确定有多少标识符是共享相同的哈希值的。如果碰撞的哈希值比较多的话,改进下你的hashCode()方法,或者扩大哈希值的取值范围。最完美的情况就是你的标记符全都有唯一的hashcode( 这其实不难实现)。优化完的好处就是,你只需要一个BitSet就够了,而不需要存储一个大的字符串集合。

总结

改进你的hashCode算法的分布。优化它比优化这个方法的执行速度要重要多了。千万不要写一个返回常量的hashCode方法。

String.hashCode的实现已经相当完美了,因此很多时候你可以用String的hashCode来代替字符串本身了。如果你使用的是字符串的set,试着把它优化成BitSet。这将大大提升你程序的性能。

文章转自 并发编程网-ifeve.com

时间: 2024-10-21 10:38:39

hashCode()方法的性能优化的相关文章

关于java hashcode方法对性能影响的一些疑问

问题描述 java api中关于hashcode的规定,就是两个值相等,他们的hashcode一定相等,但hashcode相等,其对应的值不一定相等,说hashcode相等会影响hashtables性能,具体怎么影响性能的. 解决方案 最好的办法是去阅读HashMap或者Hashtable的代码(二者的实现是比较接近的),或者研究一下数据结构里面讲述的Hash表的数据结构,HashMap&Hashtable是通过拉链法来构造的. 检索的时候使用HashCode作为Hash算法的Key,当多个Ha

Python 代码性能优化技巧分享_python

如何进行 Python 性能优化,是本文探讨的主要问题.本文会涉及常见的代码优化方法,性能优化工具的使用以及如何诊断代码的性能瓶颈等内容,希望可以给 Python 开发人员一定的参考. Python 代码优化常见技巧 代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构.优化.扩展以及文档相关的事情通常需要消耗 80% 的工作量.优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率. 改进算法,选择合适的数据结构 一个

java性能优化方案9——优化自定义hasCode()方法和equals()方法

9.优化自定义hasCode()方法和equals()方法在不能使用EnumMap的情况下,至少也要优化 hashCode() 和 equals() 方法.一个好的 hashCode() 方法是很有必要的,因为它能防止对高开销 equals() 方法多余的调用.在每个类的继承结构中,需要容易接受的简单对象.让我们看一下jOOQ的 org.jooq.Table 是如何实现的?最简单.快速的 hashCode() 实现方法如下:// AbstractTable一个通用Table的基础实现: @Ove

ASP.NET几种进行性能优化的方法及注意问题

asp.net|问题|性能|优化 网站的性能对于ASP.NET程序开发人员来说非常重要.一个优秀的网站虽然有美观的页面设计,完善的服务功能,但是打开网页时有长时间的延迟,用户最终将会无法忍受.尤其对于大型的电子商务网站而言,每秒钟有数万用户同时访问,没有良好的网站性能,根本无法满足庞大的需求. ASP.NET作为全新一代的动态网页生成系统,它在平台性能方面与原有的ASP相比已有了一个本质的提高.但要在此基础上开发出专业水准的.符合生产标准的.受用户欢迎的web应用程序,还需要开发人员从编程的角度

两个ASP的性能优化实用方法

性能|优化 1.讨论主题:ASP脚本大小 你的脚本页(还有其它页面)是不是比必须的长度要长?这是一开始执行就会降低Asp性能的东西.ASP脚本在用来获取信息和格式化输出的时候是十分有用的,但脚本也是逐行解释执行,所以你的脚本越长,执行它的时间也就越长. 如果你的脚本很庞大,怎么做才能减少脚本的长度呢?这里有几点建议:你可以将它们转换成服务器端组件,也就是说,做成VB动态链接库DLL或者通过先进的Windows编程语言或适当的COM接口语言将它转换成未编译组件?并且在服务器端注册它们.有关的快速指

MySQL order by性能优化方法实例

  这篇文章主要介绍了MySQL order by性能优化方法实例,本文讲解了MySQL中order by的原理和优化order by的三种方法,需要的朋友可以参考下 前言 工作过程中,各种业务需求在访问数据库的时候要求有order by排序.有时候不必要的或者不合理的排序操作很可能导致数据库系统崩溃.如何处理好order by排序呢?本文从原理以及优化层面介绍 order by . 一 MySQL中order by的原理 1 利用索引的有序性获取有序数据 当查询语句的 order BY 条件和

MySQL Index Condition Pushdown(ICP)性能优化方法实例

  这篇文章主要介绍了MySQL Index Condition Pushdown(ICP)性能优化方法实例,本文讲解了概念介绍.原理.实践案例.案例分析.ICP的使用限制等内容,需要的朋友可以参考下 一 概念介绍 Index Condition Pushdown (ICP)是MySQL 5.6 版本中的新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式. a 当关闭ICP时,index 仅仅是data access 的一种访问方式,存储引擎通过索引回表获取的数据会传递到MySQL Ser

MySQL延迟关联性能优化方法

  这篇文章主要介绍了MySQL延迟关联性能优化方法,本文讲解了延迟关联的背景.延迟关联的分析.延迟关联的解决等内容,需要的朋友可以参考下 [背景] 某业务数据库load 报警异常,cpu usr 达到30-40 ,居高不下.使用工具查看数据库正在执行的sql ,排在前面的大部分是: 代码如下: SELECT id, cu_id, name, info, biz_type, gmt_create, gmt_modified,start_time, end_time, market_type, b

Android性能优化以及数据优化方法_Android

Android性能优化-布局优化 今天,继续Android性能优化 一 编码细节优化. 编码细节,对于程序的运行效率也是有很多的影响的.今天这篇主题由于技术能力有限,所以也不敢在深层去和大家分享.我将这篇主题分为以下几个小节: (1)缓存 (2)数据 (3)延迟加载和优先加载 1> 缓存 在Android中缓存可以用在很多的地方:对象.IO.网络.DB等等..对象缓存能减少内存分配,IO缓存能对磁盘的读写访问,网络缓存能减少对网络的访问,DB缓存能减少对数据库的操作. 缓存针对的场景在Andro