`
xifangyuhui
  • 浏览: 189042 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

阅读更多

1.    HashMap概述:

   HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

 

2.    HashMap的数据结构:

   java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

   从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

   源码如下:

Java代码
  1. /** 
  2.  * The table, resized as necessary. Length MUST Always be a power of two. 
  3.  */  
  4. transient Entry[] table;  
  5.   
  6. static class Entry<K,V> implements Map.Entry<K,V> {  
  7.     final K key;  
  8.     V value;  
  9.     Entry<K,V> next;  
  10.     final int hash;  
  11.     ……  
  12. }  

 

   可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

 

3.    HashMap的存取实现:

   1) 存储:

 

Java代码
  1. public V put(K key, V value) {  
  2.     // HashMap允许存放null键和null值。  
  3.     // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
  4.     if (key == null)  
  5.         return putForNullKey(value);  
  6.     // 根据key的keyCode重新计算hash值。  
  7.     int hash = hash(key.hashCode());  
  8.     // 搜索指定hash值在对应table中的索引。  
  9.     int i = indexFor(hash, table.length);  
  10.     // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。  
  11.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  12.         Object k;  
  13.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  14.             V oldValue = e.value;  
  15.             e.value = value;  
  16.             e.recordAccess(this);  
  17.             return oldValue;  
  18.         }  
  19.     }  
  20.     // 如果i索引处的Entry为null,表明此处还没有Entry。  
  21.     modCount++;  
  22.     // 将key、value添加到i索引处。  
  23.     addEntry(hash, key, value, i);  
  24.     return null;  
  25. }  

 

 

   从上面的源代码中可以看出:当我们往HashMapput元素的时候,先根据keyhashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

   addEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组tablei索引处。addEntry  HashMap 提供的一个包访问权限的方法,代码如下:

 

Java代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     // 获取指定 bucketIndex 索引处的 Entry   
  3.     Entry<K,V> e = table[bucketIndex];  
  4.     // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry  
  5.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  6.     // 如果 Map 中的 key-value 对的数量超过了极限  
  7.     if (size++ >= threshold)  
  8.     // 把 table 对象的长度扩充到原来的2倍。  
  9.         resize(2 * table.length);  
  10. }  

 

 

   当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

   hash(int h)方法根据keyhashCode重新计算一次散列。此算法加入了高位计算,防止低位不变,高位变化时,造成的hash冲突。

 

Java代码
  1. static int hash(int h) {  
  2.     h ^= (h >>> 20) ^ (h >>> 12);  
  3.     return h ^ (h >>> 7) ^ (h >>> 4);  
  4. }  

 

 

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

   对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:

 

Java代码
  1. static int indexFor(int h, int length) {  
  2.     return h & (length-1);  
  3. }  

 

 

   这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2  n 次方,这是HashMap在速度上的优化。在 HashMap 构造器中有如下代码:

Java代码
  1. int capacity = 1;  
  2.     while (capacity < initialCapacity)  
  3.         capacity <<= 1;  

   这段代码保证初始化时HashMap的容量总是2n次方,即底层数组的长度总是为2n次方。

length总是 2 n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&%具有更高的效率。

   这看上去很简单,其实比较有玄机的,我们举个例子来说明:

   假设数组长度分别为1516,优化后的hash码分别为89,那么&运算后的结果如下:

       h & (table.length-1)                     hash                             table.length-1

       8 & (15-1)                                 0100                                1110                   =                0100

       9 & (15-1)                                 0101                   &              1110                   =                0100

       -----------------------------------------------------------------------------------------------------------------------

       8 & (16-1)                                 0100                   &              1111                   =                0100

       9 & (16-1)                                 0101                   &              1111                   =                0101

  

   从上面的例子中可以看出:当它们和15-11110的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,89会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-11110)进行,那么 最后一位永远是0,而0001001101011001101101111101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对keyhashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   

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

   根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key  hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry  key  hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry  key 通过equals 比较返回 true,新添加 Entry  value 将覆盖集合中原有 Entry  value,但key不会覆盖。如果这两个 Entry  key 通过 equals比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看addEntry() 方法的说明。

   2) 读取:

Java代码
  1. public V get(Object key) {  
  2.     if (key == null)  
  3.         return getForNullKey();  
  4.     int hash = hash(key.hashCode());  
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.         e != null;  
  7.         e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.             return e.value;  
  11.     }  
  12.     return null;  
  13. }  

 

   有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMapget元素时,首先计算keyhashCode,找到数组中对应位置的某一元素,然后通过keyequals方法在对应位置的链表中找到需要的元素。

  

   3) 归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry

 

4.    HashMapresizerehash):

   HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize

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

 

分享到:
评论
2 楼 xifangyuhui 2010-12-14  
buaasss 写道
你好!
请问e.hash == hash && ((k = e.key) == key || key.equals(k))这句话怎么理解?
仅仅((k = e.key) == key || key.equals(k))不行吗?
谢谢!


从逻辑上来讲,去掉e.hash==hash也是可以的吧,我估计api的作者是想保证定义的完整性,即“hash相同而且key也相同的”。
1 楼 buaasss 2010-12-12  
你好!
请问e.hash == hash && ((k = e.key) == key || key.equals(k))这句话怎么理解?
仅仅((k = e.key) == key || key.equals(k))不行吗?
谢谢!

相关推荐

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

    在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...

    深入Java集合学习系列

    文件"深入Java集合学习系列:HashMap的实现原理 - 莫等闲 - ITeye技术网站.mht"将详细解析HashMap的内部节点结构、扩容机制以及处理哈希冲突的方法。 接下来,ArrayList是一个动态数组,它继承自AbstractList并实现...

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

    HashMap 是 Java 中最常用的集合类之一,它是基于哈希表数据结构实现的,提供快速的存取操作。在深入理解 HashMap 的实现原理之前,我们先要明白哈希表的基本概念。哈希表是一种通过哈希函数将键(Key)映射到数组...

    尚硅谷-深入java8的集合3:HashMap的实现原理.pdf

    1.更适合零基础学员: ·自Java语言起源始,循序渐进,知识点剖析细致且每章配备大量随堂练习,让你步步为营,学得透彻、练得明白 ·拒绝晦涩难懂的呆板教学,宋老师语言生动幽默,举例形象生动深入浅出,迅速让你...

    【Java面试+Java学习指南】 一份涵盖大部分Java程序员所需要掌握的核心知识

    Java集合详解1:一文读懂ArrayList,Vector与Stack使用方法和实现原理 Java集合详解2:Queue和LinkedList Java集合详解3:Iterator,fail-fast机制与比较器 Java集合详解4:HashMap和HashTable Java集合详解5:深入...

    源码解析jdk7.0集合(3):HashMap的底层实现原理.pdf

    HashMap是Java集合框架中Map接口的典型实现之一,主要用于存储键值对(Key-Value)数据。其特性如下: - 线程不安全,方法为非同步方法,因此多线程环境下访问可能会出现并发问题。 - 允许使用null作为键(Key)和...

    java集合PDF汇总

    首先,我们来看"深入Java集合学习系列(一):HashMap的实现原理_尚硅谷_张晓飞.pdf"。HashMap是Java中常用的键值对存储容器,它的主要特点是查找速度快,平均时间复杂度为O(1)。HashMap内部使用了哈希表的数据结构,...

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

    【Java软件技术文档-深入Java8的集合5:Hashtable的实现原理】 在Java编程语言中,集合框架是不可或缺的一部分,而Hashtable是其中一种古老的、线程安全的哈希表实现。尽管现在更多地使用HashMap或...

    学习笔记:三数组实现的HashMap

    这里我们将深入探讨这种三数组实现HashMap的原理和可能的优势。 1. **基本概念**: - **HashMap**:HashMap是Java集合框架的一部分,它通过哈希函数将键(Key)映射到对应的值(Value),形成键值对。键必须是唯一...

    基于HashMap的用户标签处理兼Java中HashMap实现原理研究.pdf

    本文研究了基于HashMap的用户标签处理方法,并对Java中HashMap的实现原理进行了深入研究。HashMap是一种高效的数据结构,可以快速地存储和检索数据。本文通过对HashMap的分析,总结了其优缺点,并与其他存储结构进行...

    深入解读大厂java面试必考点之HashMap全套学习资料

    HashMap是Java编程语言中最常用的集合类之一,尤其在面试中,HashMap的相关知识是考察候选人对数据结构和算法理解的重要部分。本套学习资料全面涵盖了HashMap的深入解析,旨在帮助求职者掌握大厂面试中的核心知识点...

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

    K,V&gt; p) { } 这些方法是 HashMap 为 LinkedHashMap 提供的回调接口,用于在插入、访问和删除节点后执行特定的操作。LinkedHashMap 利用这些回调方法来维护其内部链表的顺序。 1. **afterNodeAccess(Node,V&gt; p)**: ...

    深入探索Java集合框架:解密复杂的面试题和精准解析

    本文将深入探讨Java集合框架的核心概念,包括List、Set、Map等接口,以及ArrayList、LinkedList、HashSet、HashMap等实现类,帮助你理解和解决面试中的相关问题。 1. **List和Set的区别及应用场景** - **List** 是...

    Java集合思维导图.xmind.zip

    这份"Java集合思维导图.xmind.zip"压缩包文件,显然旨在帮助学习者深入理解Java集合框架的核心原理以及不同版本间的差异。以下是关于Java集合类,特别是HashMap、CurrentHashMap、ArrayList和LinkedList的详细知识点...

    Java工程师面试复习指南

    Java集合详解:一文读懂ArrayList,Vector与Stack使用方法和实现原理 Java集合详解:Queue和LinkedList Java集合详解:迭代器,快速失败机制与比较器 Java集合详解:HashMap和HashTable Java集合详解:深入理解...

    Java Android开发:深入解析Java集合框架及其应用场景

    内容概要:本文详细介绍了Java集合框架的重要性和在Android开发中的应用。首先,阐述了集合框架的基本概念,包括接口(Collection、Set、List、Map)和其实现类(ArrayList、LinkedList、HashSet、TreeSet、HashMap...

    20-集合框架020-HashMap-1080P 高清-AVC20

    在这个主题中,我们将深入探讨HashMap类,它是Java集合框架中的一个关键组件,特别是在标题“20-集合框架020-HashMap-1080P 高清-AVC20”和描述中所提到的内容。 HashMap是Java.util包中的一个类,它实现了Map接口...

Global site tag (gtag.js) - Google Analytics