`
annegu
  • 浏览: 100372 次
  • 性别: Icon_minigender_2
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论
阅读更多
/**
    *@author annegu
    *@date 2009-12-02
    */

Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就正好复习一下。网上关于hashmap的文章很多,但到底是自己学习的总结,就发出来跟大家一起分享,一起讨论。

1、hashmap的数据结构
要知道hashmap是什么,首先要搞清楚它的数据结构,在java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap也不例外。Hashmap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。




从图中我们可以看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。我们来看看java代码:

/**
     * The table, resized as necessary. Length MUST Always be a power of two.
     *  FIXME 这里需要注意这句话,至于原因后面会讲到
     */
    transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        final int hash;
        Entry<K,V> next;
..........
}


        上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
         当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~

2、hash算法
我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。

所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,


 static int indexFor(int h, int length) {
        return h & (length-1);
    }


首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!





          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
          说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):

// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;




3、hashmap的resize

       当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

         那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。


4、key的hashcode与equals方法改写
在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以,hashcode与equals方法对于找到对应元素是两个关键方法。

Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也是需要改写滴~当然啦,按正常思维逻辑,equals方法一般都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。
在改写equals方法的时候,需要满足以下三点:
(1) 自反性:就是说a.equals(a)必须为true。
(2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。
(3) 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。
通过改写key对象的equals和hashcode方法,我们可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。


总结:
        本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。



这是hashmap第一篇,主要讲了一下hashmap的数据结构和计算hash的算法。接下去annegu还会写第二篇,主要讲讲LinkedHashMap和LRUHashMap。先做个预告,呵呵~
  • 大小: 78.6 KB
  • 大小: 62.9 KB
分享到:
评论
30 楼 火星来客 2009-12-03  
shelocks 写道
回复的时候没注意,上升到鸡蛋问题了,gameover?

但是由于你下面这个回复,所以这个gameover的游戏可以被重新启动,等别人去解释吧,呵呵。
shelocks 写道

  Java中采用2的幂作为容量大小的优势在于,散列函数的效率比较高,与操作肯定比mod要快。并不意味着以2的为幂的散列效果要好。

29 楼 shelocks 2009-12-03  
回复的时候没注意,上升到鸡蛋问题了,gameover?
28 楼 shelocks 2009-12-03  
zhang_xzhi_xjtu 写道
火星来客 写道
zhang_xzhi_xjtu 写道

请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?

如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。


??是为这句(不过你在上面一贴已经作了说明了):
引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。




如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。

如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。
因为java选择2的幂作为size,所以用&进行了code级别的优化。
如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。
换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。



  我同意zhang_xzhi_xjtu的观点。由于Java采用2的幂作为容量大小,所以才有与操作求索引的结论存在(与mod是一个效果),如果不采用2的幂做容量大小,与操作求索引也就不成立了,也就不会出现楼主所说的空间浪费(依旧是用与操作话)。
  至于容量大小的选择,当采用求余散列时,原则是与2的幂不太接近的质数,如果采用2的幂(p次幂)做容量的话,k mod m求余的结果是k的p个最低位数,所以hashmap中又对最初的hashcode进行了进一步的增强。
  Java中采用2的幂作为容量大小的优势在于,散列函数的效率比较高,与操作肯定比mod要快。并不意味着以2的为幂的散列效果要好。
27 楼 oyprunner 2009-12-03  
火星来客 写道
dennis_zane 写道

h&length-1,这个也算是常识吧,对数学或者位运算了解点就知道了。

&运算本身当然是常识,任何一本计算机图书中几乎都会提到,但是将其和:
int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

组合起来使用,得到%的效果,但是性能比%高,这一点我没有想到,所以这一点上对我个人来说没有将其视为常识。


组合起来使用,得到%的效果?
26 楼 火星来客 2009-12-03  
zhang_xzhi_xjtu 写道

如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。

如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。
因为java选择2的幂作为size,所以用&进行了code级别的优化。
如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。
换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。



ok,那么原理上我们没有分歧,主要是鸡蛋的问题,鸡蛋问题就不说了
25 楼 火星来客 2009-12-03  
kimmking 写道
这都精华了,
哎,我写了个介绍各种常见数据结构(ArrayList Hashmap/table。。。)的jdk实现和.net实现的原理分析的,
发到 算法和数据结构版本,被新手帖了。。。

哎,以后写啥都发java版。

你的关于hashmap文章在哪里,拜读一下,因为我有可能需要更正我在4楼说的话。
24 楼 zhang_xzhi_xjtu 2009-12-03  
火星来客 写道
zhang_xzhi_xjtu 写道

请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?

如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。


??是为这句(不过你在上面一贴已经作了说明了):
引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。




如果只是从&和%的角度看2的幂作为size效率是最高的,我同意。

如果不是2的幂,那么势必会有一些位置一直都是空的。这个我是不同意的,原因我说了,这是因果倒置。
因为java选择2的幂作为size,所以用&进行了code级别的优化。
如果不选择2的幂,则没有这个代码级别的优化,而会用%这个较慢的操作,但是这里是不会有势必会有一些位置一直都是空的这个问题的。
换言之,选择2的幂作size是&的前条件,如果不是2的幂的话,&就不成立了。(当然,还是一个hash,不过是一个很烂的hash)。


23 楼 annegu 2009-12-03  
火星来客 写道
zhang_xzhi_xjtu 写道

请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?

如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。


??是为这句(不过你在上面一贴已经作了说明了):
引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。



这句话我认为不对,如果不是2次幂的话,就不会用&了,而是用%。真正的目的就是要让元素均匀的分布在数组中,&只是在数组长度是2次幂的情况下的一种快速求模的方法。
22 楼 火星来客 2009-12-03  
zhang_xzhi_xjtu 写道

请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?

如果不是2的幂,那么势必会有一些位置一直都是空的,你的误解应该在于“效率高”这三个字是针对什么的,显然楼主是针对hashmap,并不是针对&操作,不是2的幂,那么hashmap中碰撞几率高,那么get的时候效率就低。


??是为这句(不过你在上面一贴已经作了说明了):
引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。

21 楼 zhang_xzhi_xjtu 2009-12-03  
dennis_zane 写道
zhang_xzhi_xjtu 写道
虽然精华了,但是还是要挑挑刺。

annegu 写道

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。这就是真正得原因,很多人即使看过源代码也不知道原因。

          说道÷到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):[/size]
// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;



这一段的描述是有些容易误导读者,主要原因是因为搞混了因果关系。
java的hash实现用的是除法求模的方式,是设计一个hash算法最简单的方式之一。
而return h & (length-1);只不过是当length为2的幂的时候的一种求模的代码级的优化而已。为了速度。

如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
就hash的算法而言,size是不是2的幂是不重要的,并不会出现lz说的浪费空间问题。这个是由算法决定的。

就java的实现而言,以2的幂为size的大小,是可以得到一些速度上的优势,但是也是有它自己的缺陷的。
那就是hash值的高位没有参与到hash计算中。
如 size=0x100,则对0xab11,0xac11,0xee11的hash计算结果是一样的,都为0x11。在一些特殊的输入,如高位变化,低位不变的情况下,hash冲突很严重。其原因就是只有低位参与了hash计算。

由于2在计算机程序的特殊性,以上也是一个要考虑的东西,所以大部分的算法书在介绍除法模式的hash时,都是建议用质数的。



事实上,HashMap考虑了你说到的这种情况,对hash值做了处理,加入了高位的计算:
 /**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }




这个是我按照楼主的思路去写的,没有检查java源代码,失误了。
20 楼 zhang_xzhi_xjtu 2009-12-03  
火星来客 写道
blueskit 写道
看到一本书,为了减少hashmap的冲突,内部容量长度应是优先选择“素数”,
而不是2^N.
不知道这么一点性能优势的价值能有多少。

你看的是thinking in java吧,我也曾经被误导过,第4版上写的还是1.5倍后的素数,但是他在注释中写道:经过大量测试表明2的幂作为size效率是最高的,但是遗憾的是作者并没有详细分析原因,如果分析了,那么也不会误导这么多人了,包括lss。


引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。

这个结论是哪里来的??
HashTable中是以%的方式来hash的,不过HashTable中更消耗时间是同步


请从理论上阐明2的幂作为size效率是最高的。
HashMap是用的&,HashTable用的是%,这里我的意思是说求模是算法,而如何求是实现。不知道??是问什么?
19 楼 annegu 2009-12-03  
楼主我现身啦~

zhang_xzhi_xjtu 写道

java的hash实现用的是除法求模的方式,是设计一个hash算法最简单的方式之一。
而return h & (length-1);只不过是当length为2的幂的时候的一种求模的代码级的优化而已。为了速度。


这句话是对的,用“与”操作确实是为了速度。

zhang_xzhi_xjtu 写道

如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。


这是错的!hashmap的代码里面根本没有“%”。hashmap的table[]的大小也不会是15这种不是2的次方的,table的大小一定是2次幂。看下面:

// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;
        table = new Entry[capacity];
18 楼 congdepeng 2009-12-03  
真的不错 顶一个
17 楼 kimmking 2009-12-03  
这都精华了,
哎,我写了个介绍各种常见数据结构(ArrayList Hashmap/table。。。)的jdk实现和.net实现的原理分析的,
发到 算法和数据结构版本,被新手帖了。。。

哎,以后写啥都发java版。
16 楼 blackqiqi9 2009-12-03  
哎呀!大哥
最近找工作正在学习回顾这些东西
受教了
另外 困惑于 Map 如何实现Key的快速查找?
15 楼 luckaway 2009-12-03  
zhang_xzhi_xjtu 写道
虽然精华了,但是还是要挑挑刺。

annegu 写道

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。这就是真正得原因,很多人即使看过源代码也不知道原因。

          说道÷到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):[/size]
// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;



这一段的描述是有些容易误导读者,主要原因是因为搞混了因果关系。
java的hash实现用的是除法求模的方式,是设计一个hash算法最简单的方式之一。
而return h & (length-1);只不过是当length为2的幂的时候的一种求模的代码级的优化而已。为了速度。

如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
就hash的算法而言,size是不是2的幂是不重要的,并不会出现lz说的浪费空间问题。这个是由算法决定的。

就java的实现而言,以2的幂为size的大小,是可以得到一些速度上的优势,但是也是有它自己的缺陷的。
那就是hash值的高位没有参与到hash计算中。
如 size=0x100,则对0xab11,0xac11,0xee11的hash计算结果是一样的,都为0x11。在一些特殊的输入,如高位变化,低位不变的情况下,hash冲突很严重。其原因就是只有低位参与了hash计算。

由于2在计算机程序的特殊性,以上也是一个要考虑的东西,所以大部分的算法书在介绍除法模式的hash时,都是建议用质数的。



引用
高位变化,低位不变的情况下,hash冲突很严重

高位低位先求和,然后再hash函数的吧!
14 楼 dennis_zane 2009-12-03  
zhang_xzhi_xjtu 写道
虽然精华了,但是还是要挑挑刺。

annegu 写道

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。这就是真正得原因,很多人即使看过源代码也不知道原因。

          说道÷到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):[/size]
// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;



这一段的描述是有些容易误导读者,主要原因是因为搞混了因果关系。
java的hash实现用的是除法求模的方式,是设计一个hash算法最简单的方式之一。
而return h & (length-1);只不过是当length为2的幂的时候的一种求模的代码级的优化而已。为了速度。

如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
就hash的算法而言,size是不是2的幂是不重要的,并不会出现lz说的浪费空间问题。这个是由算法决定的。

就java的实现而言,以2的幂为size的大小,是可以得到一些速度上的优势,但是也是有它自己的缺陷的。
那就是hash值的高位没有参与到hash计算中。
如 size=0x100,则对0xab11,0xac11,0xee11的hash计算结果是一样的,都为0x11。在一些特殊的输入,如高位变化,低位不变的情况下,hash冲突很严重。其原因就是只有低位参与了hash计算。

由于2在计算机程序的特殊性,以上也是一个要考虑的东西,所以大部分的算法书在介绍除法模式的hash时,都是建议用质数的。



事实上,HashMap考虑了你说到的这种情况,对hash值做了处理,加入了高位的计算:
 /**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }


13 楼 火星来客 2009-12-03  
blueskit 写道
看到一本书,为了减少hashmap的冲突,内部容量长度应是优先选择“素数”,
而不是2^N.
不知道这么一点性能优势的价值能有多少。

你看的是thinking in java吧,我也曾经被误导过,第4版上写的还是1.5倍后的素数,但是他在注释中写道:经过大量测试表明2的幂作为size效率是最高的,但是遗憾的是作者并没有详细分析原因,如果分析了,那么也不会误导这么多人了,包括lss。

引用
如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。

这个结论是哪里来的??
HashTable中是以%的方式来hash的,不过HashTable中更消耗时间是同步
12 楼 zozoh 2009-12-03  
blueskit 写道
看到一本书,为了减少hashmap的冲突,内部容量长度应是优先选择“素数”,
而不是2^N.
不知道这么一点性能优势的价值能有多少。

当你用 HashMap 做大数据量缓存的时候,你就能体会到好处了
11 楼 zhang_xzhi_xjtu 2009-12-03  
虽然精华了,但是还是要挑挑刺。

annegu 写道

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

          所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。这就是真正得原因,很多人即使看过源代码也不知道原因。

          说道÷到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):[/size]
// Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;



这一段的描述是有些容易误导读者,主要原因是因为搞混了因果关系。
java的hash实现用的是除法求模的方式,是设计一个hash算法最简单的方式之一。
而return h & (length-1);只不过是当length为2的幂的时候的一种求模的代码级的优化而已。为了速度。

如果hashmap的size是15或者其他值时,这行代码会变为return h%size;如此而已。
就hash的算法而言,size是不是2的幂是不重要的,并不会出现lz说的浪费空间问题。这个是由算法决定的。

就java的实现而言,以2的幂为size的大小,是可以得到一些速度上的优势,但是也是有它自己的缺陷的。
那就是hash值的高位没有参与到hash计算中。
如 size=0x100,则对0xab11,0xac11,0xee11的hash计算结果是一样的,都为0x11。在一些特殊的输入,如高位变化,低位不变的情况下,hash冲突很严重。其原因就是只有低位参与了hash计算。

由于2在计算机程序的特殊性,以上也是一个要考虑的东西,所以大部分的算法书在介绍除法模式的hash时,都是建议用质数的。

相关推荐

    深入理解hashmap

    深入理解hashmap、hash算法、理解加载因子、扩容以及get、put方法

    深入Java集合学习系列:HashMap的实现原理

    总之,深入理解HashMap的实现原理对于Java开发者来说至关重要。这不仅有助于写出更高效、更稳定的代码,也有助于在面试中展示出扎实的编程基础。通过本文的介绍,你应该对HashMap有了更深入的认识,包括其数据结构、...

    hashmap面试题_hashmap_

    HashMap作为Java集合框架中的重要成员,是面试中常见的知识点,尤其在数据结构与算法、并发编程以及JVM内存管理等领域,HashMap的深入理解至关重要。本篇将围绕HashMap的相关面试题,从基础概念到高级应用进行详尽...

    hashmap 实例

    在本文中,我们将深入理解 HashMap 的实例及其工作原理,并与其他数据结构如 Vector、ArrayList、LinkedList 和 Hashtable 进行对比。 首先,我们来看 HashMap 的实例代码: ```java HashMap hashmap = new ...

    用hashmap实现词典查询

    首先,我们来深入理解HashMap的工作原理。HashMap基于哈希表的概念,它通过计算元素的哈希码(hash code)将键(key)映射到数组的特定位置。当查找某个键时,HashMap会先计算键的哈希码,然后使用这个哈希码找到...

    hashmap 集合

    在深入理解HashMap之前,我们先简单回顾一下Java集合的基本概念。 Java集合框架包括Set、List和Map三个主要接口。其中,Map接口不同于Set和List,因为它不存储重复元素,而是通过键来唯一标识每个值。HashMap就是...

    HashMap讲解注释版本.java

    对HashMap 源码逐行进行注释,带你深入理解HashMap原理,使面试不在困难,

    java软件技术文档-深入java8的集合3:HashMap的实现原理.pdf

    在深入理解 HashMap 的实现原理之前,我们先要明白哈希表的基本概念。哈希表是一种通过哈希函数将键(Key)映射到数组索引位置的数据结构,以此实现快速查找。HashMap 在 Java 8 中有以下几个关键特性: 1. **非...

    深入理解Java之HashMap —— 03

    在深入探讨Java中的HashMap之前,我们先来了解一下HashMap的基本概念。HashMap是Java集合框架中的一种数据结构,它实现了Map接口,允许将键(Key)映射到值(Value)。HashMap通过哈希函数来快速定位键值对,提供O(1...

    HashMap源码剖析共10页.pdf.zip

    在深入理解HashMap的源码之前,我们首先需要了解其基本原理。 1. **HashMap的内部结构** HashMap由一个Entry对象数组构成,每个Entry对象包含了键值对的关键信息。当两个键冲突时,它们会被链接到同一个索引位置上...

    手写HashMap源码.rar

    《手写HashMap源码解析——深入理解数据结构与算法》 HashMap是Java编程语言中一个常用的集合类,它提供了一种高效、灵活的键值对存储方式。在面试过程中,尤其是2020年及以后的技术面试中,深入理解HashMap的实现...

    HashMap的特点与使用方法详解.docx

    此外,了解HashMap的工作原理,如扩容机制和散列冲突的处理,有助于优化性能和避免潜在的问题。在处理大量数据时,考虑使用`ConcurrentHashMap`以保证并发安全。最后,合理选择键值对的键,确保其重写equals和...

    HashMap的工作原理Java开发Java经验技巧共4页

    深入理解HashMap的工作原理对于提升Java开发的效率和写出高效的代码至关重要。以下是对HashMap工作原理的详细解析。 HashMap基于哈希表(也称为散列表)实现,它的核心思想是通过对象的哈希值来快速定位数据。当向...

    HashM.rar_hashmap

    标题中的"HashM.rar_hashmap"表明这是一个关于哈希映射(HashMap)的C语言实现项目,而"HashM.doc"可能是包含详细代码和解释的...这将帮助你深入理解HashMap的内部工作原理,并可能为你的C语言编程实践提供宝贵经验。

    HashMap-面试必过

    HashMap是Java中常用的一种数据结构,它用于存储键值对,是基于哈希表实现...了解这些知识点有助于深入理解HashMap的工作原理,从而在面试中自信地回答相关问题。在实际开发中,根据需求选择合适的Map实现也至关重要。

    HashMap底层实现原理共6页.pdf.zip

    在深入理解HashMap的底层实现原理之前,我们首先要明确其基本概念。 HashMap基于哈希表(也称为散列表)实现,哈希表是一种通过哈希函数将键映射到数组下标的存储结构。这种映射使得查找、插入和删除操作可以在平均...

    HashMap原理.pdf

    HashMap是Java集合框架中的一部分,是一种使用哈希表实现的Map接口。它允许存储key-value对,...对于Java初学者来说,深入理解HashMap的原理是非常重要的,这不仅有助于编写高效代码,也是进阶大型企业面试的必经之路。

    java集合类HashMap总结共7页.pdf.zip

    Java集合框架是Java编程语言中的一个核心特性,它提供了一种高效、灵活的方式来存储和操作数据。HashMap作为其中的一员,是基于哈希表...对于Java开发者来说,深入理解HashMap对于提高代码效率和解决问题能力至关重要。

    HashMap原理的深入理解

    HashMap原理的深入理解 HashMap是基于哈希表的Map接口的非同步实现,提供了所有可选的映射操作,并允许使用null值和null键。HashMap储存的是键值对,HashMap很快。此类不保证映射的顺序,特别是它不保证该顺序恒久...

Global site tag (gtag.js) - Google Analytics