(转)谨慎使用String作为HashMap的Key

标签: string 作为 hashmap | 发表时间:2013-11-23 07:38 | 作者:
出处:http://jackyrong.iteye.com
http://donlianli.iteye.com/blog/1979674

首先简单复习一下哈希表知识(大学课本定义)。
        根据设定的哈希函数f(key)和处理冲突的方法将一组关键字映像到一个有限的连续地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为哈希表。
         哈希函数f(key)是一个映像,使得任何关键字由此所得到的哈希函数值都落在表允许范围之内。
         对不同的关键字可能得到同一哈希地址,即key!=key2,但是f(key1)=f(key2),这种现象称为冲突。一般情况下,冲突只能减少,而不能完全避免。

还不清楚?请百科普及一下吧。


通过上面的复习,我们知道,决定一个哈希表的性能主要是哈希表的键值的冲突概率。如果哈希后的冲突很低,性能就高,相反,性能则低。使用一个好的哈希算法,可以降低哈希冲突的概率,提高命中率。

但是,如果被哈希的Key本身就是重复的,那么哈希算法再好,也无法避免哈希值的冲突。

我们都知道,在Java中,HashMap一般是使用对象的hashcode作为哈希的Key的。那么使用String作为HashMap的Key,好不好呢?或者,你在不知情的情况一下,已经干过很多次了。

String的hashCode方法。

Java代码  收藏代码
public int hashCode() { 
    int h = hash; 
        int len = count; 
    if (h == 0 && len > 0) { 
        int off = offset; 
        char val[] = value; 
 
            for (int i = 0; i < len; i++) { 
                h = 31*h + val[off++]; 
            } 
            hash = h; 
        } 
        return h; 
    } 


核心的代码就一行。就是

Java代码  收藏代码
h = 31*h + val[off++]; 
他的意思就是一个字符串的hashcode,就是逐个按照字符的utf-16的编码值求和。

我个人觉得,像这样的计算hashcode的话,各个字符串很容易重复(虽然我数学不好)。比如:"C9"和“Aw”
的hashcode都是2134。这样的长度为2位的字符串,我用程序统计了一下,重复的概率大概是0.6665928。

当字符长度为3个字符时,重复的概率成上升趋势,达到0.8911293,4位时为0.9739272。当然,5位长度的概率我不知道,因为我的机器上跑不出来结果。
测试代码见附1。

这么高的重复率,如果你使用它作为hashcode的话,势必会造成很大的哈希冲突,从而降低哈希表最初的设计初衷,性能降低。

但是,那String设计的时候,为啥这样设计hashcode呢?我经过测试,当字符串仅为数字时,多长的字符串,hashcode都不会重复。这是为什么呢?

从他计算的公式的31的系数看,应该是31为一个跨度,即只要字符串中的字符串的跨度在31个之内,hash值就不会重复,经过测试,确实如此。也就是说,如果你使用纯英文大写或纯英文小写字母拼接起来的字符串,其hashcode一般不会重复的。不知道这个31最初是怎么算出来的,但是,毋庸置疑,我们可以通过重新String的hashcode方法,将31改为128,那么冲突就会大大降低。

看看可能会作为Key的情况。
1、MD5,一般是字母加数字,字符跨度为75.
2、oracle的sys_guid()产生的逐渐,字符跨度为43.
3、java的UUID,跨度为75.
4、其他唯一主键情况。

建议,如果你的对象主键是上述类型,则尽量少的使用HashMap作为进行运算的工具类。

因此,当你打算使用String作为HashMap的Key时,我建议两点:
1、如果你不知道你的Key的可能的取值范围是否超过31,并且不知数量是多大时,尽量不要使用。
2、如果你对性能要求很高,请尽量不要将字符串作为主键。


附1:计算字符串重复概率的代码
Java代码  收藏代码
import java.util.HashMap; 
/**
* 测试字符串的hashcode重复几率
* @author [email protected]
*/ 
public class StringHashCode { 
     
    static HashMap<Integer,Object> map = new HashMap<Integer,Object>();  
    /**
     * 第一个可见字符
     */ 
    private static char startChar = ' ';  
    /**
     * 最后一个可见字符
     */ 
    private static char endChar = '~';  
    private static int offset = endChar - startChar + 1;  
    /**
     * 重复次数
     */ 
    private static int dupCount = 0;  
     
    public static void main(String[] args) {  
        for(int len=1;len<5;len++){ 
             char[] chars = new char[len];  
             tryBit(chars, len);  
             int total=(int)Math.pow(offset, len); 
             System.out.println(len+":"+total + ":" + dupCount+":"+map.size()+":"+(float)dupCount/total); 
        } 
         
    }  
  
    private static void tryBit(char[] chars, int i) {  
        for (char j = startChar; j <= endChar; j++) {  
            chars[i - 1] = j;  
            if (i > 1)  
                tryBit(chars, i - 1);  
            else  
                test(chars);  
        }  
    }  
  
    private static void test(char[] chars) {  
        Integer key = new String(chars).hashCode(); 
        if (map.containsKey(key)) {  
            dupCount++;  
        } else {  
            map.put(key, null);  
        }  
    }  


附2:计算字符串为长度为2的重复hashcode的代码


Java代码  收藏代码
import java.util.HashMap; 
/**
* 测试字符串的hashcode重复几率
* @author [email protected]
* 求长度为2的hashcode重复的字符串
*/ 
public class PrintStringHashCode { 
     
    static HashMap<Integer,Object> map = new HashMap<Integer,Object>();  
    /**
     * 第一个可见字符
     */ 
    private static char startChar = ' ';  
    /**
     * 最后一个可见字符
     */ 
    private static char endChar = 'z';  
    private static int offset = endChar - startChar + 1;  
    /**
     * 重复次数
     */ 
    private static int dupCount = 0;  
     
    public static void main(String[] args) {  
        int len =2; 
         char[] chars = new char[len];  
         tryBit(chars, len);  
         int total=(int)Math.pow(offset, len); 
         System.out.println(len+":"+total + ":" + dupCount+":"+map.size()+":"+(float)dupCount/total); 
    }  
  
    private static void tryBit(char[] chars, int i) {  
        for (char j = startChar; j <= endChar; j++) {  
            chars[i - 1] = j;  
            if (i > 1)  
                tryBit(chars, i - 1);  
            else  
                test(chars);  
        }  
    }  
  
    private static void test(char[] chars) {  
        String s = new String(chars); 
        Integer key = s.hashCode(); 
        if (map.containsKey(key)) {  
            dupCount++;  
            System.out.println(map.get(key)+" same :"+s+" hashcode:"+key); 
        } else {  
            map.put(key, s);  
        }  
    }  


已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [string 作为 hashmap] 推荐:

(转)谨慎使用String作为HashMap的Key

- - jackyrong
首先简单复习一下哈希表知识(大学课本定义).         根据设定的哈希函数f(key)和处理冲突的方法将一组关键字映像到一个有限的连续地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为哈希表.          哈希函数f(key)是一个映像,使得任何关键字由此所得到的哈希函数值都落在表允许范围之内.

String 常量池和 String#intern()

- - ImportNew
String是Java基础的重要考点. 可问的点多,而且很多点可以横向切到其他考点,或纵向深入JVM. 本文略过了String的基本内容,重点在于String#intern(). String常量可能会在两种时机进入常量池:. 编译期:通过双引号声明的常量(包括 显示声明、 静态编译优化后的常量,如”1”+”2”优化为常量”12”),在前端编译期将被静态的写入class文件中的“常量池”.

java HashMap的原理

- - CSDN博客研发管理推荐文章
HashMap的数据结构:    在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外. HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体.    从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表.

深入理解HashMap

- - 你好IT
Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就正好复习一下. 网上关于hashmap的文章很多,但到底是自己学习的总结,就发出来跟大家一起分享,一起讨论. 1、hashmap的数据结构. 要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap也不例外.

HashMap深度解析(一)

- - CSDN博客编程语言推荐文章
       HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,我们总会在不经意间用到它,很大程度上方便了我们日常开发. 在很多Java的笔试题中也会被问到,最常见的,“HashMap和HashTable有什么区别. ”,这也不是三言两语能说清楚的,这种笔试题就是考察你来笔试之前有没有复习功课,随便来个快餐式的复习就能给出简单的答案.

String与InputStream互相转换

- - BlogJava_首页
原文: http://www.heatpress123.net/cpzs/.

String压缩 解压缩

- - CSDN博客推荐文章
数据传输时,有时需要将数据压缩和解压缩,本例使用GZIPOutputStream/GZIPInputStream实现. 1、使用ISO-8859-1作为中介编码,可以保证准确还原数据. 2、字符编码确定时,可以在decompress方法最后一句中显式指定编码. * @return 压缩后的字符串. GZIPOutputStream os = null; // 使用默认缓冲区大小创建新的输出流.

无锁HashMap的原理与实现

- - 酷壳 - CoolShell.cn
 (本文由 onetwogoo投稿). 在《 疫苗:Java HashMap的死循环》中,我们看到,java.util.HashMap并不能直接应用于多线程环境. 对于多线程环境中应用HashMap,主要有以下几种选择:. 使用线程安全的java.util.Hashtable作为替代. 使用java.util.Collections.synchronizedMap方法,将已有的HashMap对象包装为线程安全的.

Java 8:HashMap的性能提升

- - Java译站
HashMap是一个高效通用的数据结构,它在每一个Java程序中都随处可见. 你可能也知道,HashMap使用key的hashCode()和equals()方法来将值划分到不同的桶里. 桶的数量通常要比map中的记录的数量要稍大,这样每个桶包括的值会比较少(最好是一个). 当通过key进行查找时,我们可以在常数时间内迅速定位到某个桶(使用hashCode()对桶的数量进行取模)以及要找的对象.

面试关于HashMap的工作原理

- - Java - 编程语言 - ITeye博客
几乎每个人都会回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null键值和值,而Hashtable则不 能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对等等. 这显示出你已经用过HashMap,而且 对它相当的熟悉. 但是面试官来个急转直下,从此刻开始问出一些刁钻的问题,关于HashMap的更多基础的细节.