HashMap源码解析笔记

(一)

总结:

 

0.初始化:大于initialCapacity的最小的2的n次幂,如果initailCapacity为空,取16,为什么取2的n次幂为数组长度,见ex

1.Object.hashCode() 取得32位原始int   h

2.h的高位与低位作异或处理,加大低位的随机性,掺杂了高位的部分特征,减少10%的hash碰撞

3.h与长度n-1作与处理,得出最终的数<=length-1,也是在数组中的索引

4.当map size > n * factor 时,进行扩容(2倍)

 

ex:

为什么数组长度为2的n次幂

1.length为2的整数次幂的话,h&(length-1)就相当于对length取模(取余,只有length为2的整数次幂时,此公式

h&(length-1) == h%length

才成立),来保证结果小于等于length-1,同时也提升了效率(位运算快于除法运算)

 

2.能够保证length-1每一位都是1,如此增加散列的广度,保证了散列的均匀,1&1=1  0&1=0,相反,1&0=0  0&0=0,最大限度减小空间浪费

 

 

 

 

http://blog.csdn.net/brycegao321/article/details/52527236


 

 HashMap是Java和Android程序员的基本功, JDK1.8对HashMap进行了优化, 你真正理解它了吗? 

考虑如下问题:  

1、哈希基本原理?(答:散列表、hash碰撞、链表、红黑树

2、hashmap查询的时间复杂度, 影响因素和原理? (答:最好O(1),最差O(n), 如果是红黑O(logn)

3、resize如何实现的, 记住已经没有rehash了!!!(答:拉链entry根据高位bit散列到当前位置i和size+i位置)

4、为什么获取下标时用按位与&,而不是取模%? (答:不只是&速度更快哦,  我觉得你能答上来便真正理解hashmap了)


JDK1.8的HashMap源码: http://www.grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/HashMap.java#HashMap


        我的习惯是先看注释再看源码并调试, 先翻译一下源码注释吧, 不准之处请指正哈。


Hash table based implementation of the Map interface. This implementation provides all of the optional map

   HashTable实现了Map接口类, 这些接口实现了所有可选的map功能, 包括允许空值和空key。

operations, and permits null values and thenull key. (TheHashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time. 

       HashMap和HashTable基本一致,  区别是HashMap是线程不同步的且允许空key。 HashMap不保证map的顺序, 而且顺序是可变的。

This implementation provides constant-time performance for the basic operations (getandput), assuming the hash function disperses the elements properly among the buckets.

    如果将数据适当的分散到桶里, HashMap的添加、查询函数的执行周期是常量值。

Iteration over collection views requires time proportional to the "capacity" of theHashMapinstance (the number of buckets) plus its size (the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

    使用迭代器遍历所有数据的性能跟HashMap的桶(bucket)数量有直接关系,   为了提高遍历的性能, 不能设置比较大的桶数量或者负载因子过低。

An instance of HashMap has two parameters that affect its performance:initial capacityandload factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created.

      HashMap实例有2个重要参数影响它的性能: 初始容量和负载因子。初始容量是指在哈希表里的桶总数, 一般在创建HashMap实例时设置初始容量。

The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased.

       负载因子是指哈希表在多满时扩容的百分比比例。

When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table isrehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.

      当哈希表的数据个数超过负载因子和当前容量的乘积时, 哈希表要再做一次哈希(重建内部数据结构), 哈希表每次扩容为原来的2倍。

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of theHashMap class, including get and put). 

        负载因子的默认值是0.75, 它平衡了时间和空间复杂度。 负载因子越大会降低空间使用率,但提高了查询性能(表现在哈希表的大多数操作是读取和查询)

The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

       考虑哈希表的性能问题, 要设置合适的初始容量,   从而减少rehash的次数。 当哈希表中entry的总数少于负载因子和初始容量乘积时, 就不会发生rehash动作。

If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table. Note that using many keys with the samehashCode() is a sure way to slow down performance of any hash table. To ameliorate impact, when keys arejava.lang.Comparable, this class may use comparison order among keys to help break ties. 

      如果有很多值要存储到HashMap实例中, 在创建HashMap实例时要设置足够大的初始容量, 避免自动扩容时rehash。 如果很多关键字的哈希值相同, 会降低哈希表的性能。 为了降低这个影响, 当关键字支持java.lang.Comparable时, 可以对关键字做次排序以降低影响。

Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at

least one of the threads modifies the map structurally, itmust be synchronized externally. (A structural modification

   哈希表是非线程安全的, 如果多线程同时访问哈希表, 且至少一个线程修改了哈希表的结构, 那么必须在访问hashmap前设置同步锁。(修改结构是指添加或者删除一个或多个entry, 修改键值不算是修改结构。)

is any operation that adds or deletes one or more mappings; merely changing the value associated with a key that an instance already contains is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the map. 

     一般在多线程操作哈希表时,  要使用同步对象封装map。

If no such object exists, the map should be "wrapped" using theCollections.synchronizedMap method. This is best done at creation time, to prevent accidental unsynchronized access to the map:

      如果不封装Hashmap, 可以使用Collections.synchronizedMap  方法调用HashMap实例。  在创建HashMap实例时避免其他线程操作该实例, 即保证了线程安全。

   Map m = Collections.synchronizedMap(new HashMap(...));


       JDK1.8对哈希碰撞后的拉链算法进行了优化, 当拉链上entry数量太多(超过8个)时,将链表重构为红黑树。  下面是源码相关的注释:

   * This map usually acts as a binned (bucketed) hash table, but
      * when bins get too large, they are transformed into bins of
      * TreeNodes, each structured similarly to those in
      * java.util.TreeMap. Most methods try to use normal bins, but
      * relay to TreeNode methods when applicable (simply by checking
      * instanceof a node).  Bins of TreeNodes may be traversed and
      * used like any others, but additionally support faster lookup
      * when overpopulated. However, since the vast majority of bins in
      * normal use are not overpopulated, checking for existence of
      * tree bins may be delayed in the course of table methods.


看看HashMap的几个重要成员变量:

 //The default initial capacity - MUST be a power of two.

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //为毛不写成16??? 大师是想用这种写法告诉你只能是2的幂

 HashMap的初始容量是16个, 而且容量只能是2的幂。  每次扩容时都是变成原来的2倍。


static final float DEFAULT_LOAD_FACTOR = 0.75f;

默认的负载因子是0.75f, 16*0.75=12。即默认的HashMap实例在插入第13个数据时,会扩容为32。


The bin count threshold for using a tree rather than list for a bin. Bins are converted to trees when adding an element to a bin with at least this many nodes. The value must be greater than 2 and should be at least 8 to mesh with assumptions in tree removal about conversion back to plain bins upon shrinkage.
static final int TREEIFY_THRESHOLD = 8;

注意:这是JDK1.8对HashMap的优化, 哈希碰撞后的链表上达到8个节点时要将链表重构为红黑树,  查询的时间复杂度变为O(logN)。


The table, initialized on first use, and resized as necessary. When allocated, length is always a power of two. (We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.) 
transient Node<K,V>[] table;  //HashMap的桶, 如果没有哈希碰撞, HashMap就是个数组,我说的是如果吐舌头  数组的查询时间复杂度是O(1),所以HashMap理想时间复杂度是O(1);如果所有数据都在同一个下标位置, 即N个数据组成链表,时间复杂度为O(n), 所以HashMap的最差时间复杂度为O(n)。如果链表达到8个元素时重构为红黑树,而红黑树的查询时间复杂度为O(logN), 所以这时HashMap的时间复杂度为O(logN)。


Holds cached entrySet(). Note that AbstractMap fields are used for keySet() and values().
transient Set<Map.Entry<K,V>> entrySet; //HashMap所有的值,因为用了Set, 所以HashMap不会有key、value都相同的数据。

                                

                                                       哈希表的结构

1、 哈希碰撞的原因和解决方法:

     哈希碰撞是不同的key值找到相同的下标,  对应HashMap里hashcode和容量的模相同。

源码629行    if ((p = tab[i = (n - 1) & hash]) == null) , 其中n是容量值,    即用哈希值和容量相与得到要保存的位置。 如果不同Key的(n - 1) & hash相同, 那么要存储到同一个数组下标位置, 这个现象就叫哈希碰撞。

        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {

          ....

         if ((p = tab[i = (n - 1) & hash]) == null)     //如果该下标没值,则存储到该下标位置
             tab[i] = newNode(hash, key, value, null);      
         else {
             Node<K,V> e; K k;
             if (p.hash == hash &&
                 ((k = p.key) == key || (key != null && key.equals(k))))
                 e = p;      //如果哈希值相同而且key相同, 则更新键值
             else if (p instanceof TreeNode)
                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  //如果下标数据是TreeNode类型,则将新数据添加到红黑树中。
             else {
                 for (int binCount = 0; ; ++binCount) {
                     if ((e = p.next) == null) {
                         p.next = newNode(hash, key, value, null);   //将新Node添加到链表末尾
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             treeifyBin(tab, hash);    //如果链表个数达到8个时,将链表修改为红黑树结构
                         break;
                     }     

            .....

           }


2、JDK1.8对HashMap最大的优化是resize函数,  在扩容时不再需要rehash了, 下面就看看大师是怎么实现的。

Initializes or doubles table size. If null, allocates in accord with initial capacity target held in field threshold. Otherwise, because we are using power-of-two expansion, the elements from each bin must either stay at same index, or move with a power of two offset in the new table.

初始化数组或者扩容为2倍,   初值为空时,则根据初始容量开辟空间来创建数组。否则, 因为我们使用2的幂定义数组大小,数据要么待在原来的下标, 或者移动到新数组的高位下标。 (举例: 初始数组是16个,假如有2个数据存储在下标为1的位置, 扩容后这2个数据可以存在下标为1或者16+1的位置)

Returns:
    the table
final Node<K,V>[] resize() {

         ....

         newThr = oldThr << 1; // double threshold,  大小扩大为2倍,出于性能考虑和者告诉使用者它是2的幂, 这里用的是位移, 而不是*2,


   if (e.next == null)
      newTab[e.hash & (newCap - 1)] = e;  //如果该下标只有一个数据,则散列到当前位置或者高位对应位置(以第一次resize为例,原来在第4个位置,resize后会存储到第4个或者第4+16个位置)
  else if (e instanceof TreeNode)
     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  //红黑树重构

   else {

     do {
        next = e.next;
        if ((e.hash & oldCap) == 0) {    
            if (loTail == null)
               loHead = e;
            else
            loTail.next = e;
            loTail = e;
        } else {
            if (hiTail == null)
               hiHead = e;
            else
               hiTail.next = e;
               hiTail = e;
         }
      } while ((e = next) != null);
      if (loTail != null) {
          loTail.next = null;
          newTab[j] = loHead;   //下标不变
      }
      if (hiTail != null) {
          hiTail.next = null;
          newTab[j + oldCap] = hiHead; //下标位置移动原来容量大小
      }

   (e.hash & oldCap) == 0写的很赞!!! 它将原来的链表数据散列到2个下标位置,  概率是当前位置50%,高位位置50%。     你可能有点懵比, 下面举例说明。  上边图中第0个下标有496和896,  假设它俩的hashcode(int型,占4个字节)是

resize前:

496的hashcode: 00000000  00000000  00000000  00000000

896的hashcode: 01010000  01100000  10000000  00100000

oldCap是16:       00000000  00000000  00000000  00010000

    496和896对应的e.hash & oldCap的值为0, 即下标都是第0个。


resize后:

 

496的hashcode: 00000000  00000000  00000000  00000000

896的hashcode: 01010000  01100000  10000000  00100000

oldCap是32:       00000000  00000000  00000000  00100000

   496和896对应的e.hash & oldCap的值为0和1, 即下标都是第0个和第16个。

   看明白了吗?   因为hashcode的第n位是0/1的概率相同, 理论上链表的数据会均匀分布到当前下标或高位数组对应下标。

       回顾JDK1.7的HashMap,在扩容时会rehash即每个entry的位置都要再计算一遍,  性能不好呀, 所以JDK1.8做了这个优化。

      再回到文章最开始的问题, HashMap为什么用&得到下标,而不是%?   如果使用了取模%, 那么在容量变为2倍时, 需要rehash确定每个链表元素的位置。大笑

     很佩服HashMap的作者呀,  大师在运算符的使用上都是这么考究!!!






 

(二)http://blog.csdn.net/caimengyuan/article/details/61204542



 

转载自知乎的话题 https://www.zhihu.com/question/28562088


这段代码叫“扰动函数”。 
题主贴的是Java 7的HashMap的源码,Java 8中这步已经简化了,只做一次16位右位移异或混合,而不是四次,但原理是不变的。下面以Java 8的源码为例解释,

//Java 8中的散列值优化函数
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    //key.hashCode()为哈希算法,返回初始哈希值
}
  • 1
  • 2
  • 3
  • 4
  • 5

大家都知道上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。

理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。

但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。源码中模运算是在这个indexFor( )函数里完成的。

bucketIndex = indexFor(hash, table.length);
indexFor的代码也很简单,就是把散列值和数组长度做一个"与"操作,
static int indexFor(int h, int length) {
        return h & (length-1);
}
  • 1
  • 2
  • 3
  • 4
  • 5

顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

        10100101 11000100 00100101
    &   00000000 00000000 00001111
----------------------------------
        00000000 00000000 00000101    //高位全部归零,只保留末四位
  • 1
  • 2
  • 3
  • 4

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。

这时候“扰动函数”的价值就体现出来了,说到这里大家应该猜出来了。看下面这个图, 



右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

最后我们来看一下Peter Lawley的一篇专栏文章《An introduction to optimising a hashing strategy》里的的一个实验:他随机选取了352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标。 



结果显示,当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,
在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。

但明显Java 8觉得扰动做一次就够了,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。

 

小结顺序:

1.Object.hashCode() 取得32位原始int   h

2.h的高位与低位作异或处理

3.h与长度n-1作与处理,得出最终在数组中的索引

4.当map size > n * factor 时,进行扩容(2倍)






(三)http://www.cnblogs.com/fangfuhai/p/6420645.html

                              

   Entry类是HashMap的内部类,其实现了Map.Entry接口。Entry类里定义了4个属性:Object类型的key、value(K、V类型可以看成Object类型),Entry类型的next属性(这个next其实就是一个指向下一个Entry对象的引用,形成了一个链表,通过此Entry对象的next属性可以找到其下一个Entry对象)和int型的hash值。HashMap底层维护的就是一个个Entry对象。在Entry类里还重写了equals方法,若两个Entry的key和value都相等,则返回true,否则返回false,同时还重写了hashCode方法。


如果key不为null,首先根据key的hashCode值计算出hash值,根据hash值和数组长度计算出要存放到数组中的位置i,然后遍历table[i]处的链表,如果链表上存在元素其hash值与计算得到的hash值相等并且其key值与新增的key相等(通过核对key的equels函数),那么就以新增的value覆盖此元素原来的value并返回原来的value值;如果链表上不存在满足上面条件的元素,则将key-value生成的Entry对象存放到table[i]处,并将其next指向此处原来的Entry对象。这样经过多次put操作,就构成了数组加链表的存储结构。



根据key的hashCode值来计算出一个hash值,然后根据hash值和数组长度计算出一个数组下标值,接着循环遍历此下标处的单链表,寻找满足条件的Entry对象并返回value,此value就是HashMap中该key所映射的value。注意分析当key为null时的情况:如果HashMap中有key为null的映射关系,那么就返回null映射的value,否则就表明HashMap中不存在key为null的映射关系,返回null。同理,当get方法返回的值为null时,并不一定表明该映射不包含该键的映射关系,也可能是该映射将该键显示的映射为null,即put(key, null)。可使用containKey方法来区分这两种情况


 

从以上源码的分析中我们知道了HashMap底层维护的是数组加链表的混合结构,这是HashMap的核心,只要掌握了这一点我们就能很容易弄清楚HashMap中映射关系的各种操作原理,其本质是对数组和链表的操作。要注意的是HashMap不是线程安全的,我们可以使用Collections.synchoronizedMap方法来获得线程安全的HashMap。例如:

       Map map = Collections.sychronizedMap(new HashMap());

 





(四)http://www.cnblogs.com/ITtangtang/p/3948406.html


允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变

 

HashMap底层是通过链表来解决hash冲突的。




我们可以看到在构造HashMap的时候如果我们指定了加载因子和初始容量的话就调用第一个构造方法,否则的话就是用默认的。默认初始容量为16,默认加载因子为0.75。我们可以看到上面代码中13-15行,这段代码的作用是确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂红色部分写出了为什么

 

h & (length-1);  //这里不能随便算取,用hash&(length-1)是有原因的,这样可以确保算出来的索引是在数组大小范围内,不会超出

 

 

这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。



接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值   0&1=0 1&1=1),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,(0&0=0 1&0=0)因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。(精辟)


 

而当数组长度为16时,即为2的n次方时,n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同(提供既高效又纯粹的取模,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

 

那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,扩容是需要进行数组复制的,复制数组是非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能


如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。






(五)

关于hashcode 里面 使用31 系数的问题

http://blog.csdn.net/tayanxunhua/article/details/20525251

 

 

首先我们来了解一下hashcode,什么是hashcode?有什么作用?

hashcode其实就是散列码,hashcode使用高效率的哈希算法来定位查找对象!

我们在使用容器来存储数据的时候会计算一串散列码,然后将数据放入容器。

如:String s =“java”,那么计算机会先计算散列码,然后放入相应的数组中,数组的索引就是从散列码计算来的,然后再装入数组里的容器里,如List.这就相当于把你要存的数据分成了几个大的部分,然后每个部分存了很多值, 你查询的时候先查大的部分,再在大的部分里面查小的,这样就比先行查询要快很多!

一个对象的HashCode就是一个简单的Hash算法的实现,虽然它和那些真正的复杂的Hash算法相比还不能叫真正的算法,但如何实现它,不仅仅是程序员的编程水平问题, 而是关系到你的对象在存取时性能的非常重要的问题.有可能,不同HashCode可能 会使你的对象存取产生成百上千倍的性能差别!

java String在打印这个类型的实例对象的时候总是显示为下面的形式

test.Test$tt@c17164

上面test.Test是类名$tt是我自己写的内部类,而@后面这一段是什么呢?他其实就是tt这个实例类的hashcode的16进制!

它使用了Object 里面的toString()方法

      Java代码:
  1. return getClass().getName() + “@” + Integer.toHexString(hashCode());  
继续看看java里 String hashcode的源码:
[java] view plain copy
 
  1. public int hashCode() {  
  2.     int h = hash;  
  3.     if (h == 0 && value. length > 0) {  
  4.         char val[] = value;  
  5.   
  6.         for ( int i = 0; i < value. length; i++) {  
  7.             h = 31 * h + val[i];  
  8.         }  
  9.         hash = h;  
  10.     }  
  11.     return h;  
  12. }  



其实上面的实现也就是数学表达式的实现:

Java代码 
  1. s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]  
      s[i]是string的第i个字符,n是String的长度。
这个怎么算出来的呢?
看一下这个步骤吧(参考上面的源码,进行for循环,然后写下每步):
[java] view plain copy
 
  1. String str = "abcd";  
  2.   
  3. h = 0  
  4. value.length = 4  
  5.   
  6. val[0] = a  
  7. val[1] = b  
  8. val[2] = c  
  9. val[3] = d  
  10.   
  11. h = 31*0 + a  
  12.   = a  
  13.   
  14. h = 31 * (31*0 + a) + b  
  15.   = 31 * a + b  
  16.   
  17. h = 31 * (31 * (31*0 + a) + b) + c  
  18.   = 31 * (31 * a + b) + c  
  19.   = 31 * 31 * a + 31 * b + c  
  20.   
  21. h = 31 * (31 * 31 * a + 31 * b + c) + d  
  22.   = 31 * 31 * 31 * a + 31 * 31 * b + 31 * c + d  
  23.    
  24. h = 31 ^ (n-1) * val[0] + 31 ^ (n-2) * val[1] + 31 ^ (n-3) * val[2] + ...+ val[n-1]  



我们会注意那个狗血的31这个系数为什么总是在里面乘来乘去?为什么不适用32或者其他数字?

大家都知道,计算机的乘法涉及到移位计算。当一个数乘以2时,就直接拿该数左移一位即可!选择31原因是因为31是一个素数!

所谓素数:

质数又称素数。指在一个大于1的自然数中,除了1和此整数自身外,没法被其他自然数整除的数。

在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长(31 = 11111[2])的系数并且让乘法尽量不要溢出(如果选择大于11111的数,很容易溢出)的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。

31可以 由i*31== (i<<5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits!

在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失.

而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数的原因不过与此!

 



 



 

posted on 2017-10-23 11:06  silyvin  阅读(701)  评论(0编辑  收藏  举报