`
deepinmind
  • 浏览: 451404 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
1dc14e59-7bdf-33ab-841a-02d087aed982
Java函数式编程
浏览量:41620
社区版块
存档分类
最新评论

hashCode的性能优化

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

hashCode()方法的主要目的就是使得一个对象能成为hashMap的key或者存储到hashset中。这种情况下对象还得实现equals(Object)方法,它的实现和hashCode()必须是一致的:

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


hashCode的性能


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

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

我们来看下不同质量的hashcode的区别是什么。我们拿一个正常的String和一个String的包装类进行比较,它重写了hashCode方法,所有的对象返回的是同一个hashCode。


private static class SlowString
{
    public final String m_str;
 
    public SlowString( final String str ) {
        this.m_str = str;
    }
 
    @Override
    public int hashCode() {
        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"(确保字符串的中间部分变化而结尾不变,不会影响hashCode的质量)。我们会创建1M,5M,10M,20M个字符串,来看下有多少个字符串是共享hashcode的,同一个hashcode又被多少个字符串共享。下面是测试的结果:

Number of duplicate hashCodes for 1000000 strings = 0

Number of duplicate hashCodes for 5000000 strings = 196
Number of hashCode duplicates = 2 count = 196

Number of duplicate hashCodes for 10000000 strings = 1914
Number of hashCode duplicates = 2 count = 1914

Number of duplicate hashCodes for 20000000 strings = 17103
Number of hashCode duplicates = 2 count = 17103


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

自动生成long字段的hashCode方法

值得一提的是,大多数IDE是如何自动生成long类型的hashcode方法的。下面是一个生成的hashCode方法,这个类有两个long字段。

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


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

public int hashCode() {
    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 hashCode() {
    int result = (int) val1;
    result = 31 * result + (int) (val1 >>> 32);
    result = 31 * result + (int) val2;
    return 31 * result + (int) (val2 >>> 32);
}


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


<table summary="日志级别">
      <thead>
        <tr>
            <td>Two longs with original hashCode</td>
            <td>Two longs with modified hashCode</td>
            <td>Two ints</td>
        </tr>
        </thead>
    <tbody>
        <tr>
            <td>2.596 sec</td>
            <td>1.435 sec</td>
            <td> 0.737 sec</td>
        </tr>
</tbody>
</table>


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


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


假设我们有一个map,它是由String标识符指向某个特定的值。map的key(String标识符)只会存储在这个map中(同一时间,最多有一部分是存储在别的地方)。假设我们已经收集了map的所有entry,比如说在某个两阶段算法中的第一个阶段。第二个阶段我们需要通过key来查找map。我们只会用已经map里存在的key进行查找。

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


Map<Integer, Integer> cnt = new HashMap<Integer, Integer>( max );
for ( final String s : dict.keySet() )
{
    final int hash = s.hashCode();
    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 hashCode = entry.getKey().hashCode();
    if ( mult.containsKey( hashCode ) )
        not_unique.put( entry.getKey(), entry.getValue() );
    else
        unique.put( hashCode, 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 hashCode = key.hashCode();
    Object value = m_unique.get( hashCode );
    if ( value == null )
        value = m_not_unique.get( key );
    return value;
}



在一些不太常见的情况下,你的不唯一的map里的对象可能会很多。遇见这种情况的话,先尝试用java.util.zip.CRC32或者是java.util.zip.Adler32来替换掉hashCode的实现方法(Adler32比CRC32要快,不过它的分布较差些)。如果实在不行,尝试用两个不同的函数来计算hashcode:低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<Integer>,不唯一的hashCode得到的是一个Set<String>。这里至少能优化掉不少字符串的空间。

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

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


总结

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

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


原创文章转载请注明出处:http://it.deepinmind.com

英文原文链接

2
1
分享到:
评论

相关推荐

    10种java性能优化方案.docx

    ### Java性能优化方案详解 #### 一、理解性能优化的重要性 在现代软件开发中,特别是在Java领域,性能优化是一项至关重要的任务。随着系统的复杂性和规模不断增长,优化不仅仅是提高用户体验那么简单,更是确保...

    J2EE程序的性能优化技巧

    然而,随着用户数量的增加,性能优化成为确保系统稳定运行的关键因素。本文将深入探讨J2EE程序性能优化的一些关键技术和策略。 一、概要 J2EE平台的核心在于其分层架构,它支持分布式计算,简化了应用开发。通过...

    深入 HashCode 方法~

    - 为了减少冲突,可以通过优化 `HashCode` 的计算方法,使不同的对象尽可能获得不同的 `HashCode` 值。 #### 四、HashCode 的注意事项 1. **HashCode 与 equals 的一致性**: - 如果两个对象相等(根据 `equals...

    Java性能优化的45个细节

    Java性能优化是提升系统效率的关键环节,涉及到代码编写、内存管理、并发处理等多个方面。以下是对"Java性能优化的45个细节"的详细解读: 1. **选择正确的数据结构**:根据业务需求,合理选用ArrayList、LinkedList...

    hashcode的作用

    - 如果两个对象的 `hashCode()` 相同,尽管这可能导致哈希冲突,但是通过良好的设计和优化,依然可以保持高效的性能。 2. **解决哈希冲突**: - 哈希冲突是指不同的对象映射到了相同的哈希值上。为了避免过多的...

    Java哈希表性能优化

    ### Java哈希表性能优化 #### 一、引言 随着现代软件开发中对高性能数据结构的需求日益增加,Java作为一种广泛应用的编程语言,其内置的数据结构优化变得尤为重要。Java的`java.util`包中提供了多种数据结构,其中...

    深入HashCode

    《深入HashCode》 在计算机科学领域,特别是在Java和许多其他面向对象编程语言中,`hashCode()`方法是一个至关重要的概念。...通过合理设计和优化`hashCode()`方法,我们可以提高程序的性能并确保数据结构的正确性。

    利用反射绕过编译器和hashcode高级应用

    在Java编程语言中,反射(Reflection)是一种强大的工具,它允许程序在运行时检查和操作类、接口、字段以及方法等。...同样,正确实现和使用hashcode也是优化程序性能和保证数据结构正确性的关键。

    HashCode相同equals不同的2位字符集合算法

    总之,理解并正确实现`hashCode()`和`equals()`对于优化Java集合框架的性能和避免逻辑错误至关重要。在处理特定情况,如2位字符集合时,需要特别注意哈希冲突的可能性,并采取适当的策略来减少它们。通过这种方式,...

    Java编程性能优化技巧有哪些共7页.pdf.zip

    在Java编程世界里,性能优化是一项至关重要的任务,它直接影响着程序的运行效率、资源消耗以及用户体验。以下是一些常见的Java编程性能优化技巧,这些技巧覆盖了代码编写、内存管理、垃圾回收、并发处理等多个方面。...

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

    在Java编程语言中,ArrayList和HashSet是两种常用的...理解这两个数据结构的特点以及如何利用Hashcode优化性能,对于提高代码效率和可维护性至关重要。学习和掌握这些基础知识,将有助于成为一名更优秀的Java开发者。

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

    但是,两个不相等的对象可能会有相同的哈希码,这是一种允许的冲突情况,但应尽可能减少冲突以优化哈希表的性能。 Java的`Objects.equals()`和`Objects.hash()`方法是帮助我们更安全地处理`equals()`和`hashCode()`...

    Java_重写equals()和hashCode()

    在Java编程语言中,`equals()` 和 `hashCode()` 方法是对象的基本组成部分,它们在很多场景下都发挥着至关重要的作用。...通过遵循上述原则和最佳实践,我们可以确保对象的比较行为符合预期,并优化程序性能。

    深入HashCode方法

    总的来说,深入理解并恰当使用HashCode方法对于优化Java程序的性能至关重要,特别是在处理大量数据和频繁查找操作的场景下。正确设计和实现HashCode方法能够显著提升HashMap和Hashtable等数据结构的性能,从而提高...

    hashCode内存溢出和内存泄漏的问题解决.docx

    内存管理和优化是编程中至关重要的环节,尤其是...总之,理解和解决内存泄漏问题对于优化程序性能和稳定性至关重要。通过遵循最佳实践和使用现代开发工具,可以有效地避免和修复这类问题,从而确保应用程序的健康运行。

    hashcode()和equals()

    在Java编程语言中,`hashCode()` 和 `equals()`...了解并正确使用 `hashCode()` 和 `equals()` 是编写高质量Java代码的关键,特别是在处理集合框架和优化查找性能时。通过遵循上述原则,可以确保程序的稳定性和高效性。

    Android开发性能优化总结

    在Android开发中,性能优化是提升用户体验的关键环节。本文将从加载、缓存、异步处理、防止内存溢出(OOM)、View优化、内存泄漏检测以及高效数据结构使用等方面进行详细的探讨。 首先,加载优化包括预加载和懒加载...

    Java程序性能优化之二十三个建议

    在Java程序性能优化方面,有23个关键的建议可以帮助我们提升代码的效率,减少资源消耗,从而提高系统的整体性能。以下是对这23个建议的详细解释: 1. **理解对象创建成本**:Java中创建对象需要内存分配和构造函数...

    Java toString的性能优化方案比较

    谁在关心toString的性能?没有人!除非当你有大量的数据在批量处理,使用toString产生了许多日志。然后,你去调查为何如此之慢,才意识到大部分的toString方法使用的是introspection,它其实是可以被优化的。  ...

    如何正确实现Java中的HashCode共6页.pdf.z

    在Java编程语言中,`hashCode...综上所述,正确实现`hashCode()`方法对于优化Java应用程序的性能和正确性至关重要。在设计和实现类时,必须考虑到`hashCode()`与`equals()`的一致性,以确保哈希表操作的正确性和效率。

Global site tag (gtag.js) - Google Analytics