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()方法,所有对象都返回同一个哈希值。
01
|
private static class SlowString
|
02
|
{
|
03
|
public final String m_str;
|
04
|
05
|
public SlowString( final String str ) {
|
06
|
this .m_str = str;
|
07
|
}
|
08
|
09
|
@Override
|
10
|
public int hash Code() {
|
11
|
return 37 ;
|
12
|
}
|
13
|
14
|
@Override
|
15
|
public boolean equals(Object o) {
|
16
|
if ( this == o) return true ;
|
17
|
if (o == null || getClass() != o.getClass()) return false ;
|
18
|
final SlowString that = ( SlowString ) o;
|
19
|
return !(m_str != null ? !m_str.equals(that.m_str) : that.m_str != null );
|
20
|
}
|
21
|
}
|
下面是一个测试方法。后面我们还会再用到它,所以这里还是简单介绍一下 。它接收一个对象列表,然后对列表中的每个元素依次调用Map.put(), Map.containsKey()方法。
01
|
private static void testMapSpeed( final List lst, final String name )
|
02
|
{
|
03
|
final Map<Object, Object> map = new HashMap<Object, Object>( lst.size() );
|
04
|
int cnt = 0 ;
|
05
|
final long start = System.currentTimeMillis();
|
06
|
for ( final Object obj : lst )
|
07
|
{
|
08
|
map.put( obj, obj );
|
09
|
if ( map.containsKey( obj ) )
|
10
|
++cnt;
|
11
|
}
|
12
|
final long time = System.currentTimeMillis() - start;
|
13
|
System.out.println( "Time for " + name + " is " + time / 1000.0 + " sec, cnt = " + cnt );
|
14
|
}
|
String和SlowString对象都是按照”ABCD”+i的格式生成的。处理100000个String对象需要0.041秒,而处理SlowString对象则需要82.5秒。
结果表明,String类的hashCode()方法明显胜出。我们再做另一个测试。先创建一个字符串列表,前半部分的格式是”ABCdef*&”+i,后半部分的是”ABCdef*&”+i+”ghi”(确保字符串的中间部分变化而结尾不变,不会影响哈希值的质量)。我们会创建1百万,5百万,1千万,2千万个字符串,来看下有多少字符串是共享哈希值的,同一个哈希值又会被多少个字符串共享。下面是测试的结果:
01
|
Number of duplicate hash Codes for 1000000 strings = 0
|
02
|
03
|
Number of duplicate hash Codes for 5000000 strings = 196
|
04
|
Number of hash Code duplicates = 2 count = 196
|
05
|
06
|
Number of duplicate hash Codes for 10000000 strings = 1914
|
07
|
Number of hash Code duplicates = 2 count = 1914
|
08
|
09
|
Number of duplicate hash Codes for 20000000 strings = 17103
|
10
|
Number of hash Code duplicates = 2 count = 17103
|
可以看到,共用同一个哈希值的字符串很少,而一个哈希值被两个以上的字符串共享的概率则非常小。当然了,你的测试数据可能不太一样——如果用这个测试程序测试你给定的字符串的话。
自动生成long字段的hashCode()方法
许多IDE生成long类型的hashcode()的方式非常值得一提。下面是一个生成的hashCode()方法,这个类有两个long类型的字段。
01
|
Number of duplicate hash Codes for 1000000 strings = 0
|
02
|
03
|
Number of duplicate hash Codes for 5000000 strings = 196
|
04
|
Number of hash Code duplicates = 2 count = 196
|
05
|
06
|
Number of duplicate hash Codes for 10000000 strings = 1914
|
07
|
Number of hash Code duplicates = 2 count = 1914
|
08
|
09
|
Number of duplicate hash Codes for 20000000 strings = 17103
|
10
|
Number of hash Code duplicates = 2 count = 17103
|
下面给只有两个int类型的类生成的方法:
1
|
public int hash Code() {
|
2
|
int result = val1;
|
3
|
result = 31 * result + val2;
|
4
|
return result;
|
5
|
}
|
可以看到,long类型的处理是不一样的。java.util.Arrays.hashCode(long a[])用的也是同样的方法。事实上,如果你将long类型的高32位和低32位拆开当成int处理的话,生成的hashCode的分布会好很多。下面是两个long字段的类的改进后的hasCode方法(注意,这个方法运行起来比原来的方法要慢,不过新的hashCode的质量会高很多,这样的话hash集合的执行效率会得到提高,虽然hashCode本身变慢了)。
1
|
public int hash Code() {
|
2
|
int result = ( int ) val1;
|
3
|
result = 31 * result + ( int ) (val1 >>> 32 );
|
4
|
result = 31 * result + ( int ) val2;
|
5
|
return 31 * result + ( int ) (val2 >>> 32 );
|
6
|
}
|
下面是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,计算出它们的哈希值,找出那些不唯一的哈希值:
01
|
Map<Integer, Integer> cnt = new HashMap<Integer, Integer>( max );
|
02
|
for ( final String s : dict.keySet() )
|
03
|
{
|
04
|
final int hash = s.hash Code();
|
05
|
final Integer count = cnt.get( hash );
|
06
|
if ( count != null )
|
07
|
cnt.put( hash, count + 1 );
|
08
|
else
|
09
|
cnt.put( hash, 1 );
|
10
|
}
|
11
|
12
|
//keep only not unique hash codes
|
13
|
final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 );
|
14
|
for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() )
|
15
|
{
|
16
|
if ( entry.getValue() > 1 )
|
17
|
mult.put( entry.getKey(), entry.getValue() );
|
18
|
}
|
现在我们可以创建两个新的map。为了简单点,假设map里存的值就是Object。在这里,我们创建了Map<Integer, Object> 和Map<String, Object>(生产环境推荐使用TIntObjectHashMap)两个map。第一个map存的是那些唯一的hashcode以及对应的值,而第二个,存的是那些hashCode不唯一的字符串以及它们相应的值。
01
|
final Map<Integer, Object> unique = new HashMap<Integer, Object>( 1000 );
|
02
|
final Map<String, Object> not_unique = new HashMap<String, Object>( 1000 );
|
03
|
04
|
//dict - original map
|
05
|
for ( final Map.Entry<String, Object> entry : dict.entrySet() )
|
06
|
{
|
07
|
final int hash Code = entry.getKey().hash Code();
|
08
|
if ( mult.containsKey( hash Code ) )
|
09
|
not_unique.put( entry.getKey(), entry.getValue() );
|
10
|
else
|
11
|
unique.put( hash Code, entry.getValue() );
|
12
|
}
|
13
|
14
|
//keep only not unique hash codes
|
15
|
final Map<Integer, Integer> mult = new HashMap<Integer, Integer>( 100 );
|
16
|
for ( final Map.Entry<Integer, Integer> entry : cnt.entrySet() )
|
17
|
{
|
18
|
if ( entry.getValue() > 1 )
|
19
|
mult.put( entry.getKey(), entry.getValue() );
|
20
|
}
|
现在,为了查找某个值,我们得先查找第一个hashcode唯一的map,如果没找到,再查找第二个不唯一的map:
1
|
public Object get( final String key )
|
2
|
{
|
3
|
final int hash Code = key.hash Code();
|
4
|
Object value = m_unique.get( hash Code );
|
5
|
if ( value == null )
|
6
|
value = m_not_unique.get( key );
|
7
|
return value;
|
8
|
}
|
在一些不太常见的情况下,你的这个不唯一的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。这将大大提升你程序的性能。
本文最早发表于Java译站
hashCode()方法的性能优化相关推荐
- Java中String对象的replaceAll方法调用性能优化小技巧
Java中String对象的replaceAll方法调用性能优化小技巧 0x01 Java中String对象的replaceAll方法调用性能优化小技巧 1.1 What? 1.2 Why? 1.3 ...
- java split()方法_Java 性能优化的 50 个细节(珍藏版)
作 者:Java杂记 来 源:yq.aliyun.com/articles/662001 在Java程序中,性能问题的大部分原因并不在于Java语言,而是程序本身.养成良好的编码习惯非常重要,能够显著 ...
- js延迟加载的几种方法(性能优化defer、async)
这是一个面试经常问到的问题:js的延迟加载方法 (js的延迟加载有助于提高页面的加载速度) 主要考察对程序的性能方面是否有研究,程序的性能是一个项目不断地追求的,通常也是项目完成后需要长期做的一件事情 ...
- MySQL索引使用方法和性能优化
关于MySQL索引的好处,如果正确合理设计并且使用索引的MySQL是一辆兰博基尼的话,那么没有设计和使用索引的MySQL就是一个人力三轮车.对于没有索引的表,单表查询可能几十万数据就是瓶颈,而通常大型 ...
- delphi 算术溢出解决方法_性能优化系列:JVM 内存划分总结与内存溢出异常详解分析...
前言 那些使用过 C 或者 C++ 的读者一定会发现这两门语言的内存管理机制与 Java 的不同.在使用 C 或者 C++ 编程时,程序员需要手动的去管理和维护内存,就是说需要手动的清除那些不需要的对 ...
- android电量优化方法,Android性能优化——电池使用优化
为什么要做电量优化 Android应用开发中,需要考虑的情况是,如何优化电量使用,让我们的app不会因为电量消耗过高被用户排斥,或者被其他安全应用报告. 什么样的行为会导致电量损耗过高 对于移动设备而 ...
- mysql 系统参数优化方法_Mysql 性能优化2 系统参数配置方法 和 文件系统
--------------------------------------------目录------------------------------------------------- • 关于 ...
- 性能优化指南:性能优化的一般性原则与方法
作为一个程序员,性能优化是常有的事情,不管是桌面应用还是web应用,不管是前端还是后端,不管是单点应用还是分布式系统.本文从以下几个方面来思考这个问题:性能优化的一般性原则,性能优化的层次,性能优化的 ...
- 性能优化的一般性原则和方法
看了一篇详细介绍性能优化的博文,感觉很不错,特转载自此.原文地址:性能优化的一般性原则和方法 正文 作为一个程序员,性能优化是常有的事情,不管是桌面应用还是web应用,不管是前端还是后端,不管是单点应 ...
最新文章
- Win64 驱动内核编程-3.内核里使用内存
- alv tree 总结
- IDEA如何生成get和set方法
- 分类算法中常用的评价指标
- FFMPEG 源码分析
- 富士康欲进军电动汽车市场 目标占据市场10%份额
- 算法:同构字符串205. Isomorphic Strings
- sublime下编辑LaTeX
- MySQL 定时备份数据库(非常全)
- 免费的推广APP方案
- 红色警戒2修改器原理百科(九)
- 中电海康校招面试数据存储与处理事业部
- nutch核心代码分析——crawl.injector总结
- 快速学习-登录功能实现-LoginServlet
- Telnet,DHCP,静态路由
- 手机控制软件-Total_Control
- 将扩散模型应用到文本领域
- java.lang.IllegalArgumentException: Schema specific part is opaque
- 深度学习常用模型总结(思维导图形式)
- C++类:获取地方恒星时
热门文章
- 计算机狐狸标志的程序,小狐狸等分线计算工具
- java如何捕获多个异常_是否可以在单个catch块中捕获多个Java异常?
- 软件测试文档在哪里,软件测试报告技术文档
- 初学linux系统代码,linux初学者-系统日志(二)(示例代码)
- linux中postscript如何生成,【转载】如何为Linux生成和打上patch
- tcp当主动发出syn_一文读懂TCP四次挥手工作原理及面试常见问题汇总
- 银行的相关计算机知识,银行计算机基础知识试题及答案正式版.doc
- (10) ejb学习: Jpa的JTA事务和RESOURCE_LOCAL事务
- java面试题三十 public,private,protected,default访问权限
- java面试题一 静态变量