`

java hashMap解读

 
阅读更多

javaHashMap详解

 

     HashMap HashSet Java Collection Framework 的两个重要成员,其中 HashMap Map 接口的常用实现类,HashSet Set 接口的常用实现类。虽然 HashMap HashSet 实现的接口规范不同,但它们底层的 Hash 存储机制完全一样,甚至 HashSet 本身就采用 HashMap 来实现的。

通过 HashMapHashSet 的源代码分析其 Hash 存储机制


实际上,HashSet HashMap 之间有很多相似之处,对于 HashSet 而言,系统采用 Hash 算法决定集合元素的存储位置,这样可以保证能快速存、取集合元素;对于 HashMap 而言,系统 key-value 当成一个整体进行处理,系统总是根据 Hash 算法来计算 key-value 的存储位置,这样可以保证能快速存、取 Map key-value 对。

在介绍集合存储之前需要指出一点:虽然集合号称存储的是 Java 对象,但实际上并不会真正将 Java 对象放入 Set 集合中,只是在 Set 集合中保留这些对象的引用而言。也就是说:Java 集合实际上是多个引用变量所组成的集合,这些引用变量指向实际的 Java 对象。

集合和引用

就像引用类型的数组一样,当我们把 Java 对象放入数组之时,并不是真正的把 Java 对象放入数组中,只是把对象的引用放入数组中,每个数组元素都是一个引用变量。

HashMap 的存储实现


当程序试图将多个 key-value 放入 HashMap 中时,以如下代码片段为例:

Java代码

1.   HashMap<String , Double> map = new HashMap<String , Double>();   

2.   map.put("语文" , 80.0);   

3.   map.put("数学" , 89.0);   

4.   map.put("英语" , 78.2);  

HashMap 采用一种所谓的“Hash 算法来决定每个元素的存储位置。

当程序执行 map.put("语文" , 80.0); 时,系统将调用"语文" hashCode() 方法得到其 hashCode ——每个 Java 对象都有 hashCode() 方法,都可通过该方法获得它的 hashCode 值。得到这个对象的 hashCode 值之后,系统会根据该 hashCode 值来决定该元素的存储位置。

我们可以看 HashMap 类的 put(K key , V value) 方法的源代码:

Java
代码
 public V put(K key, V value)
 {
     //
如果 key null,调用 putForNullKey 方法进行处理
     if (key == null)
         return putForNullKey(value);
     //
根据 key keyCode 计算 Hash
     int hash = hash(key.hashCode());
     //
搜索指定 hash 值在对应 table 中的索引
      int i = indexFor(hash, table.length);
     //
如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素
     for (Entry<K,V> e = table[i]; e != null; e = e.next)
     {
         Object k;
         //
找到指定 key 与需要放入的 key 相等(hash 值相同
         //
通过 equals 比较放回 true
         if (e.hash == hash && ((k = e.key) == key
             || key.equals(k)))
         {
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             return oldValue;
         }
     }
     //
如果 i 索引处的 Entry null,表明此处还没有 Entry
     modCount++;
     //
keyvalue 添加到 i 索引处
     addEntry(hash, key, value, i);
     return null;
 }

上面程序中用到了一个重要的内部接口:Map.Entry,每个 Map.Entry 其实就是一个 key-value 对。从上面程序中可以看出:当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。

上面方法提供了一个根据 hashCode() 返回值来计算 Hash 码的方法:hash(),这个方法是一个纯粹的数学计算,其方法如下:

Java
代码
static int hash(int h)
{
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 Hash 码值总是相同的。接下来程序会调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下:
Java
代码
static int indexFor(int h, int length)
{
    return h & (length-1);
}
这个方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置—— HashMap 底层数组的长度总是 2 n 次方,这一点可参看后面关于 HashMap 构造器的介绍。

length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:假设 h=5,length=16, 那么 h & length - 1 将得到 5;如果 h=6,length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 , length=16 时,那么 h & length - 1 将得到 1 ……这样保证计算得到的索引值总是位于 table 数组的索引之内。

根据上面 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() 方法的说明。

当向 HashMap 中添加 key-value 对,由其 key hashCode() 返回值决定该 key-value 对(就是 Entry 对象)的存储位置。当两个 Entry 对象的 key hashCode() 返回值相同时,将由 key 通过 eqauls() 比较值决定是采用覆盖行为(返回 true),还是产生 Entry 链(返回 false)。

上面程序中还调用了 addEntry(hash, key, value, i); 代码,其中 addEntry HashMap 提供的一个包访问权限的方法,该方法仅用于添加一个 key-value 对。下面是该方法的代码:

1.   void addEntry(int hash, K key, V value, int bucketIndex)   

2.   {   

3.       // 获取指定 bucketIndex 索引处的 Entry   

4.       Entry<K,V> e = table[bucketIndex];     //   

5.       // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry   

6.       table[bucketIndex] = new Entry<K,V>(hash, key, value, e);   

7.       // 如果 Map 中的 key-value 对的数量超过了极限  

8.       if (size++ >= threshold)   

9.           //  table 对象的长度扩充到 2 倍。  

10.        resize(2 * table.length);    //   

11.}  


上面方法的代码很简单,但其中包含了一个非常优雅的设计:系统总是将新添加的 Entry 对象放入 table 数组的 bucketIndex 索引处——如果 bucketIndex 索引处已经有了一个 Entry 对象,那新添加的 Entry 对象指向原有的 Entry 对象(产生一个 Entry 链),如果 bucketIndex 索引处没有 Entry 对象,也就是上面程序号代码的 e 变量是 null,也就是新放入的 Entry 对象指向 null,也就是没有产生 Entry 链。

JDK 源码

JDK 安装目录下可以找到一个 src.zip 压缩文件,该文件里包含了 Java 基础类库的所有源文件。只要读者有学习兴趣,随时可以打开这份压缩文件来阅读 Java 类库的源代码,这对提高读者的编程能力是非常有帮助的。需要指出的是:src.zip 中包含的源代码并没有包含像上文中的中文注释,这些注释是笔者自己添加进去的。

Hash
算法的性能选项

根据上面代码可以看出,在同一个 bucket 存储 Entry 链的情况下,新放入的 Entry 总是位于 bucket 中,而最早放入该 bucket 中的 Entry 则位于这个 Entry 链的最末端。

上面程序中还有这样两个变量:

    * size
:该变量保存了该 HashMap 中所包含的 key-value 对的数量。
    * threshold
:该变量包含了 HashMap 能容纳的 key-value 对的极限,它的值等于 HashMap 的容量乘以负载因子(load factor)。

从上面程序中号代码可以看出,当 size++ >= threshold 时,HashMap 会自动调用 resize 方法扩充 HashMap 的容量。每扩充一次,HashMap 的容量就增大一倍。

上面程序中使用的 table 其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是 HashMap 的容量。HashMap 包含如下几个构造器:

    * HashMap()
:构建一个初始容量为 16,负载因子为 0.75 HashMap
    * HashMap(int initialCapacity)
:构建一个初始容量为 initialCapacity,负载因子为 0.75 HashMap
    * HashMap(int initialCapacity, float loadFactor)
:以指定初始容量、指定的负载因子创建一个 HashMap

当创建一个 HashMap 时,系统会自动创建一个 table 数组来保存 HashMap 中的 Entry,下面是 HashMap 中一个构造器的代码:

Java代码

 

1.   // 以指定初始化容量、负载因子创建 HashMap   

2.    public HashMap(int initialCapacity, float loadFactor)   

3.    {   

4.        // 初始容量不能为负数  

5.        if (initialCapacity < 0)   

6.            throw new IllegalArgumentException(   

7.           "Illegal initial capacity: " +   

8.                initialCapacity);   

9.        // 如果初始容量大于最大容量,让出示容量  

10.     if (initialCapacity > MAXIMUM_CAPACITY)   

11.         initialCapacity = MAXIMUM_CAPACITY;   

12.     // 负载因子必须大于 0 的数值  

13.     if (loadFactor <= 0 || Float.isNaN(loadFactor))   

14.         throw new IllegalArgumentException(   

15.         loadFactor);   

16.     // 计算出大于 initialCapacity 的最小的 2  n 次方值。  

17.     int capacity = 1;   

18.     while (capacity < initialCapacity)   

19.         capacity <<= 1;   

20.     this.loadFactor = loadFactor;   

21.     // 设置容量极限等于容量 * 负载因子  

22.     threshold = (int)(capacity * loadFactor);   

23.     // 初始化 table 数组  

24.     table = new Entry[capacity];            //   

25.     init();   

26. }  


上面代码中粗体字代码包含了一个简洁的代码实现:找出大于 initialCapacity 的、最小的 2 n 次方值,并将其作为 HashMap 的实际容量(由 capacity 变量保存)。例如给定 initialCapacity 10,那么该 HashMap 的实际容量就是 16
程序号代码处可以看到:table 的实质就是一个数组,一个长度为 capacity 的数组。

对于 HashMap 及其子类而言,它们采用 Hash 算法来决定集合中元素的存储位置。当系统开始初始化 HashMap 时,系统会创建一个长度为 capacity Entry 数组,这个数组里可以存储元素的位置被称为桶(bucket,每个 bucket 都有其指定索引,系统可以根据其索引快速访问该 bucket 里存储的元素。

无论何时,HashMap 的每个只存储一个元素(也就是一个 Entry),由于 Entry 对象可以包含一个引用变量(就是 Entry 构造器的的最后一个参数)用于指向下一个 Entry,因此可能出现的情况是:HashMap bucket 中只有一个 Entry,但这个 Entry 指向另一个 Entry ——这就形成了一个 Entry 链。如图 1 所示:

 

  1. HashMap 的存储示意

HashMap
的读取实现

HashMap 的每个 bucket 里存储的 Entry 只是单个 Entry ——也就是没有通过指针产生 Entry 链时,此时的 HashMap 具有最好的性能:当程序通过 key 取出对应 value 时,系统只要先计算出该 key hashCode() 返回值,在根据该 hashCode 返回值找出该 key table 数组中的索引,然后取出该索引处的 Entry,最后返回该 key 对应的 value 即可。看 HashMap 类的 get(K key) 方法代码:

Java
代码

1.   public V get(Object key)   

2.   {   

3.    // 如果 key  null,调用 getForNullKey 取出对应的 value   

4.    if (key == null)   

5.        return getForNullKey();   

6.    // 根据该 key  hashCode 值计算它的 hash   

7.    int hash = hash(key.hashCode());   

8.    // 直接取出 table 数组中指定索引处的值,  

9.    for (Entry<K,V> e = table[indexFor(hash, table.length)];   

10.     e != null;   

11.     // 搜索该 Entry 链的下一个 Entr   

12.     e = e.next)         //   

13. {   

14.     Object k;   

15.     // 如果该 Entry  key 与被搜索 key 相同  

16.     if (e.hash == hash && ((k = e.key) == key   

17.         || key.equals(k)))   

18.         return e.value;   

19. }   

20. return null;   

21.}  


从上面代码中可以看出,如果 HashMap 的每个 bucket 里只有一个 Entry 时,HashMap 可以根据索引、快速地取出该 bucket 里的 Entry;在发生“Hash 冲突的情况下,单个 bucket 里存储的不是一个 Entry,而是一个 Entry 链,系统只能必须按顺序遍历每个 Entry,直到找到想搜索的 Entry 为止——如果恰好要搜索的 Entry 位于该 Entry 链的最末端(该 Entry 是最早放入该 bucket 中),那系统必须循环到最后才能找到该元素。

归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据 Hash 算法来决定其存储位置;当需要取出一个 Entry 时,也会根据 Hash 算法找到其存储位置,直接取出该 Entry。由此可见:HashMap 之所以能快速存、取它所包含的 Entry,完全类似于现实生活中母亲从小教我们的:不同的东西要放在不同的位置,需要时才能快速找到它。

当创建 HashMap 时,有一个默认的负载因子(load factor),其默认值为 0.75,这是时间和空间成本上一种折衷:增大负载因子可以减少 Hash 表(就是那个 Entry 数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap get() put() 方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加 Hash 表所占用的内存空间。

掌握了上面知识之后,我们可以在创建 HashMap 时根据实际需要适当地调整 load factor 的值;如果程序比较关心空间开销、内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕则可以适当的减少负载因子。通常情况下,程序员无需改变负载因子的值。

如果开始就知道 HashMap 会保存多个 key-value 对,可以在创建时就使用较大的初始化容量,如果 HashMap Entry 的数量一直不会超过极限容量(capacity * load factor),HashMap 就无需调用 resize() 方法重新分配 table 数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为 capacity Entry 数组),因此创建 HashMap 时初始化容量设置也需要小心对待。

 

From http://chenyabiao0706.blog.163.com/

 

分享到:
评论

相关推荐

    java笔记与java核心内容解读

    5. **集合框架**:Java集合框架包括List、Set、Map接口及其实现类,如ArrayList、LinkedList、HashSet、HashMap等。掌握它们的特性和用法对于编写高效代码非常有用。 6. **多线程**:Java内置对多线程的支持,包括...

    HashMap之put方法源码解读.docx

    HashMap 之 put 方法源码解读 HashMap 是 Java 中一种常用的数据结构,用于存储键值对。其中,put 方法是 HashMap 中最重要的方法之一,负责将键值对存储到HashMap 中。在本文中,我们将对 HashMap 的 put 方法的...

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

    HashMap是Java编程语言中最常用的集合类之一,尤其在面试中,HashMap的相关知识是考察...这套学习资料应该包含了HashMap的实例分析、源码解读、常见面试题以及实战演练等内容,确保你全面掌握这一核心Java数据结构。

    (003)HashMap中红黑树TreeNode的split方法源码解读.docx

    HashMap 中红黑树 TreeNode 的 split 方法源码解读 HashMap 中红黑树 TreeNode 的 split 方法是 Java 中HashMap 的核心组件之一,负责将红黑树从旧数组转移到新数组上,并进行树链表的重新组织和优化。在本文中,...

    java源码解读-java-src:java源码解读

    Java源码解读是Java开发人员深入理解平台工作原理和编程模型的重要途径。在这个"java-src:java源码解读"项目中,我们可以探索Java的核心库,包括JVM(Java虚拟机)、集合框架、并发机制、I/O流、网络编程等多个关键...

    深入解读大厂java面试必考基本功-HashMap集合配套文档代码资料

    在本套课程中,将会非常深入、非常详细、非常全面的解读HashMap以及源码底层设计的思想。从底层的数据结构到底层源码分析以及怎样使用提高HashMap集合的效率问题等进行分析。如果掌握本套课程,那么再看其他javase的...

    java源码解读-JavaSource:Java源码解读

    在Java编程语言的世界里,源码解读是提升技术深度、理解内部机制的关键步骤。"JavaSource:Java源码解读"项目旨在帮助开发者深入探索Java的内部工作原理,从而更好地运用和优化代码。在这个项目中,我们可以看到一...

    java编程200例(附:JAVA文档完全解读中文版)

    "Java编程200例(附:JAVA文档完全解读中文版)"是一个非常适合初学者和进阶者的学习资源,它提供了丰富的实例来帮助理解Java的核心概念和技术。 这200个编程实例涵盖了Java语言的基础到高级主题,包括但不限于: ...

    通俗易懂而不失深度 HashMap解读

    HashMap是Java编程中常用的一种数据结构,用于存储键值对(Key-Value)的数据。它在Java集合框架中属于Map接口的一个实现类,提供高效、灵活的存储和访问功能。HashMap的设计结合了数组和链表(以及红黑树)的优势,...

    java源码解读-JavaAPI:jdk源码解读分析

    本篇文章将对Java API的部分关键组件进行源码解读,帮助读者深入理解其工作原理。 1. **对象创建与内存管理**: - `Object`类:所有Java类的基类,包含了如`clone()`, `equals()`, `hashCode()`等方法。理解`...

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

    解读Java中的回调 反射 泛型 枚举类 Java注解和最佳实践 JavaIO流 多线程 深入理解内部类 javac和javap Java8新特性终极指南 序列化和反序列化 继承、封装、多态的实现原理 容器 Java集合类总结 Java集合详解1:一文...

    (006)HashMap$TreeNode确保根节点为头节点的moveRootToFront方法源码解读.docx

    在Java的集合框架中,HashMap是一个非常重要的数据结构,它提供了高效的存储和查找元素的能力。在HashMap的实现中,为了优化性能,当链表长度达到一定阈值时,会将链表转换为红黑树(Red-Black Tree)。红黑树是一种...

    本仓库记录了我的Java学习进阶之路,涵盖了Java基础、JDK源码、JVM中的重要知识,附有代码和博客讲解,旨在提供一个Java在线共享学习平台,帮助更多的Java学习入门者进阶 .zip

    作者目录Java基础Java基础学习(1)——引用Java基础学习(2)——注解Java基础学习(3)...解读(2)——HashMapJava集合框架源码解读(3)——LinkedHashMapJava集合框架源码解读(4)——WeakHashMapJava集合框架源码解读(5)—

    HashMap源码粗略解读(面试必问)

    这篇文章将对HashMap的一些核心知识点进行深入解读,特别关注于面试中常见的问题。 1. **HashMap的默认容量** HashMap的默认容量是16,这是通过构造函数中的`initialCapacity`参数指定的,如果未显式设置,则...

    清华妹子的Java仓库(进阶学习路线)

    本仓库记录了我的Java学习进阶之路,涵盖了Java基础、JDK源码、JVM中...Java集合框架源码解读(2)——HashMap Java集合框架源码解读(3)——LinkedHashMap Java集合框架源码解读(4)——WeakHashMap Java集合框架源码解读

    java源码解读-ITG-JavaBook01:Java面试高频源码解读

    《Java源码解读-ITG-JavaBook01: Java面试高频源码解读》是一部针对Java程序员面试准备的深入学习资料。在这个项目中,我们将会探索Java语言的一些核心概念和常用库的源代码,帮助开发者更好地理解Java的内部机制,...

    Java工程师面试复习指南

    解读Java中的回调 反射 泛型 枚举类 Java注解和最佳实践 JavaIO流 多线程 深入理解内部类 javac和javap Java8新特性终极指南 序列化和反序列化 继承封装多态的实现原理 集合类 Java集合类总结 Java集合详解:一文读...

    Java中HashSet的解读_.docx

    在Java编程语言中,HashSet是一种基于哈希表(HashMap)实现的无序、不重复的集合类。它的核心特点是高效查找、添加和删除元素。在深入解析HashSet之前,我们需要了解其内部使用的HashMap的工作原理。 HashMap是...

    java面试题_源码解读(3题)

    在Java面试中,源码解读是一项重要的能力,它考察了开发者对Java语言底层实现的理解以及问题解决的能力。这里我们将深入探讨三道常见的Java面试题,它们涵盖了基础、并发和集合框架等方面,帮助你提升对Java源码的...

Global site tag (gtag.js) - Google Analytics