HashMap是Java Collection Framework 的重要成员之一。HashMap是基于哈希表的 Map 接口的实现,此实现提供所有可选的映射操作,映射是以键值对的形式映射:key-value。key——此映射所维护的键的类型,value——映射值的类型,并且允许使用 null 键和 null 值。而且HashMap不保证映射的顺序。

    简单的介绍一下HashMap,就开始HashMap的源码分析。

    首先简单的介绍一下HashMap里都包含的数据结构。觉得还是先贴一张图比较好,结合图文会比较好理解一些。

  

    现在就可以开说一下这三种数据结构。

    第一个就是Node<K,V>类型的节点。

      备注:static class HashMap.Node<K,V> implements Map.Entry<K,V>

    第二个就是由Node<K,V>类型组成的一个Node<K,V>[] table数组。

    第三个就是一个TreeNode<K,V>类型的红黑树。

      备注:static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
         static class LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V>
         static class HashMap.Node<K,V> implements Map.Entry<K,V>

    现在结合代码看一看这三种数据结构(其实Node和Node[] 是同一类,所以就是两种)。
    

    第一种:Node<K,V>

    看这个之前先看看它实现的父类接口Map.Entry<K,V>的源码(Entry是Map接口里的一个内部接口)
      

  1.       interface Entry<K,V> {
  2.           K getKey();
  3.           V getValue();
  4.           V setValue(V value);
  5.           boolean equals(Object o);
  6.           int hashCode();
  7.       }

    再看Node的源码(Node是HashMap类的一个静态内部类,实现了Map接口里的内部接口Entry)
     

  1.  static class Node<K,V> implements Map.Entry<K,V> {
  2.         //这四个成员变量就是一个Node节点所包含的四个变量域
  3.         //其中hash的计算方法是由一个hash方法得到的,该方法的是实现就是HashMap里,这里为了看的清楚一些,就写到下面:
  4.         // hash = (h = key.hashCode()) ^ (h >>> 16);
  5.           final int hash;
  6.           final K key;
  7.           V value;
  8.           Node<K,V> next;
  9.         
        
            //构造方法
  10.         Node(int hash, K key, V value, Node<K,V> next) {
  11.           this.hash = hash;
  12.           this.key = key;
  13.           this.value = value;
  14.           this.next = next;
  15.         }
  16.         //拿到该Node 的key
  17.         public final K getKey() { return key; }
  18.         //拿到该Node 的value
  19.         public final V getValue() { return value; }
  20.         //重写toString方法
  21.         public final String toString() { return key + "=" + value; }

        //修改当前Entry对象的value为传进入newvalue,返回值为之前的oldvalue值      

         public final V setValue(V newValue) {      

             V oldValue = value;       

              value = newValue;        

             return oldValue;   

         }       

            /*       

            判断两个Entry是否相等     

            若两个Entry的“key”和“value”都相等,则返回true;否则返回false     

            */      

           public final boolean equals(Object o) {        

                 if (o == this)    

                   return true;       

                 if (o instanceof Map.Entry) {         

                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;

                   if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))       

                        return true;       

                 }       

                  return false;

          }      

           //重写了equals,因此重新实现hashCode()       

            public final int hashCode() {     

                return Objects.hashCode(key) ^ Objects.hashCode(value);  

            }   

        }

  

    第二种:TreeNode(这里只附上TreeNode类的成员变量和一个获取树根的方法,其他的省略。因为TreeNode里实现了很多关于红黑树的操作方法,如果全部放到这里不仅看不懂而且显得特别乱)

  1. static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
  2. TreeNode<K,V> parent;
  3. TreeNode<K,V> left;
  4. TreeNode<K,V> right;
  5. TreeNode<K,V> prev;
  6. //构造函数
  7. TreeNode(int hash, K key, V val, Node<K,V> next){
         super(hash, key, val, next);
  8. }
  9.  
  10. //返回该树的树根
  11. final TreeNode<K,V> root() {
  12.   for (TreeNode<K,V> r = this, p;;){
  13.   if ((p = r.parent) == null) return r; r = p;
         }
  14.    }
  15. //其余部分省略,有兴趣的可以自行查看源代码。
  16.  
  17. }

    然后在说一下TreeNode构造函数所做的事情。

    先了解一下TreeNode的继承体系:TreeNode继承了LinkedHashMap.Entry
                    LinkedHashMap.Entry继承了HashMap.Node
                    HashMap.Node实现了Map.Entry

    了解这个就能清楚的知道TreeNode的构造函数到底做了什么事情。底下的图显示了TreeNode利用构造初始化的流程。
      

    其实最终还是调到了Node的构造,初始化TreeNode了从Node继承而来的四个数据域(int hash,K key,V value,Node<K,V> next)。
  

    接着我们说一下HashMap里几个重要的成员变量和常量(省略了一部分)。
     

  1.      //通过HahMap实现的接口可知,其支持所有映射操作,能被克隆,支持序列化
  2.       public class HashMap<K,V> extends AbstractMap<K,V>
  3.             implements Map<K,V>, Cloneable, Serializable {
  4.  
  5.       private static final long serialVersionUID = 362498820763181265L;
  6.  
  7.       //默认Node<K,V> table的初始容量16,2的4次方。
  8.       static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  9.  
  10.       //table数组的最大容量为1073 741 824,2的30次方。
  11.       static final int MAXIMUM_CAPACITY = 1 << 30;
  12.  
  13.       /*
  14.         默认加载因子
  15.         该值和table的长度的乘积为是否对table进行扩容的标志。
  16.         例如当该值默认为0.75,table的长度为16时,就是说当table填充至 12*0.75 =12 之后,这个table就要进行扩容
  17.       */
  18.       static final float DEFAULT_LOAD_FACTOR = 0.75f;
  19.  
  20.       //哈希表的定义,这个数据结构在前面已经介绍过
  21.       transient Node<K,V>[] table;
  22.  
  23.       /*
  24.         HashMap的大小,即保存的键值对的数量
  25.         当该值大于等于HashMap的阈值时,数组就会扩充
  26.       */
  27.       transient int size;
  28.  
  29.       /*
  30.         HashMap的阈值.
  31.         用于判断是否需要调整HashMap的容量
  32.         该值的计算方法为: 加载因子 * (table.length)
  33.         当table.size>=threshold就会扩容,就是如果hashMap中存放的键值对大于这个阈值,就进行扩容
  34.         如果加载因子没有被分配,默认为0.75
  35.         如果table数组没有被分配,默认为初始容量值(16);
  36.         或若threshold值为0,也表明该值为初始容量值。
  37.       */
  38.       int threshold;
  39.  
  40.       /*
  41.         加载因子
  42.         如果加载因子没有被分配,默认为0.75
  43.       */
  44.       final float loadFactor;

  45.       //当添加到tab[i]位置下的链表长度达到8时将链表转换为红黑树
  46.      static final int TREEIFY_THRESHOLD = 8;
  47.  
  48.         ......
  49.     }

    介绍完数据结构再来说一下我们平时经常写的代码:Map<String,Object> map = new HashMap<String,Object>()

    (这样的方式获得的map对象,调用的是默认无参的构造,实际上还有其他三个有参的构造函数)

    要知道写完这句代码之后到底发生了什么。我们首先就得看一下HashMap构造器(共4个构造器),源码如下

  1. /* 
            默认无参构     
          构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
      */
  2. public HashMap() {
  3. this.loadFactor = DEFAULT_LOAD_FACTOR;
  4. }
  5.  
  6. //构造一个带指定初始容量和加载因子的空 HashMap。
  7. public HashMap(int initialCapacity, float loadFactor) {
  8.    //初始容量为负数,抛出异常。
  9.    if (initialCapacity < 0)
  10.      throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  11.   //初始容量大于最大容量,把初始容量定为最大容量。
  12.    if (initialCapacity > MAXIMUM_CAPACITY)
  13.      initialCapacity = MAXIMUM_CAPACITY;

  14.      //如果加载因子不符合规范,抛出异常
  15.    if (loadFactor <= 0 || Float.isNaN(loadFactor))
  16.       throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
  17.     //经过一些过滤和处理,现在的loadFactor和initialCapacity都为合法的值
  18.     //初始化加载因子
  19.     this.loadFactor = loadFactor;
  20.     /*
  21.       初始化HashMap的阈值
  22.       调用tableSizeFor(initialCapacity)方法
  23.       该方法根据数组的初始容量的大小求出HashMapde的阈值threshold 
        */
  24.     this.threshold = tableSizeFor(initialCapacity);
  25.   }
  26.  
  27.   /*
  28.     第三个构造方法
  29.     构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
  30.   */
  31.   public HashMap(int initialCapacity) {
  32.     this(initialCapacity, DEFAULT_LOAD_FACTOR);
  33.   }
  34.  
  35.   // 构造一个映射关系与指定 Map 相同的新 HashMap。
  36.   public HashMap(Map<? extends K, ? extends V> m) {
  37.       this.loadFactor = DEFAULT_LOAD_FACTOR;
  38.       putMapEntries(m, false);
  39.   }

    上面提到了根据数组初始容量得出HashMap阈值的一个方法:tableSizeFor(int cap)。该方法的源码如下:

      

  1.       static final int tableSizeFor(int cap) {
  2.           int n = cap - 1;
  3.           n |= n >>> 1;
  4.           n |= n >>> 2;
  5.           n |= n >>> 4;
  6.           n |= n >>> 8;
  7.           n |= n >>> 16;
  8.           return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  9.       }

  
      简单的测试了一下这个方法

      

  1.     public class TestHashMap{
            
            static final int MAXIMUM_CAPACITY = 1 << 30;
  2.         public static void main(String[] args){
  3.  
  4.           System.out.println("---------------------");
  5.           System.out.println(" 第一次测试,cap = 0,返回的阈值threshold = "+tableSizeFor(0));
  6.           System.out.println("---------------------");
  7.           System.out.println(" 第二次测试,cap = 3,返回的阈值threshold = "+tableSizeFor(3));
  8.           System.out.println("---------------------");
  9.           System.out.println(" 第三次测试,cap = 16,返回的阈值threshold = "+tableSizeFor(16));
  10.           System.out.println("---------------------");
  11.           System.out.println(" 第四次测试,cap = 100,返回的阈值threshold = "+tableSizeFor(100));
  12.           System.out.println("---------------------");
  13.           System.out.println(" 第五次测试,cap = 1000,返回的阈值threshold = "+tableSizeFor(1000));
  14.           System.out.println("---------------------");
  15.           System.out.println(" 第六次测试,cap = 10000,返回的阈值threshold = "+tableSizeFor(10000));
  16.           System.out.println("---------------------");
  17.  
  18.         }
  19.  
  20.         static final int tableSizeFor(int cap) {
  21.           int n = cap - 1;
  22.          
  23.           n |= n >>> 1;
  24.         
  25.           n |= n >>> 2;
  26.          
  27.           n |= n >>> 4;
  28.       
  29.           n |= n >>> 8;
  30.         
  31.           n |= n >>> 16;
  32.         
  33.           return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  34.         }
  35.       }


   
 输出结果:
    ---------------------

    第一次测试,cap = 0,返回的阈值threshold = 1
    --------------------
    第二次测试,cap = 3,返回的阈值threshold = 4
    ---------------------
    第三次测试,cap = 16,返回的阈值threshold = 16
    ---------------------
    第四次测试,cap = 100,返回的阈值threshold = 128
    ---------------------
    第五次测试,cap = 1000,返回的阈值threshold = 1024
    ---------------------
    第六次测试,cap = 10000,返回的阈值threshold = 16384
    ---------------------

    总之,tableSizeFor方法是根据table数组的容量计算出hashMap可以维护出的键值对。从结果可以看到阈值至少是1,而且是刚好比cap大一点的2的幂。

    至于为什么阈值要做成这样,看了很多文章,都是说为了使hashMap散列均匀。但是实际上tab[i]中i的选择是 hash & lenth-1 是和容量在做按位或。

    因此对这里有一些疑惑。

    到此对HashMap一部分介绍完了。哪里有不对的地方,还望指出。还有就是如果能解答我的疑惑,还请指导,十分感谢 ^_^。

    

  1.  

OpenJDK1.8.0 源码解析————HashMap的实现(一)的更多相关文章

  1. OpenJDK1.8.0 源码解析————HashMap的实现(二)

    上一篇文章介绍了HashMap的一部分的知识,算是为下面HashMap的进一步学习做准备吧. 然后写的时候一直在思考的一个问题是,这方面的知识网上的资料也是一抓一大把,即使是这样我为什么还要花费时间去 ...

  2. solr&lucene3.6.0源码解析(四)

    本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...

  3. Android事件总线(二)EventBus3.0源码解析

    1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus ...

  4. JDK8源码解析 -- HashMap(二)

    在上一篇JDK8源码解析 -- HashMap(一)的博客中关于HashMap的重要知识点已经讲了差不多了,还有一些内容我会在今天这篇博客中说说,同时我也会把一些我不懂的问题抛出来,希望看到我这篇博客 ...

  5. solr&lucene3.6.0源码解析(三)

    solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...

  6. Heritrix 3.1.0 源码解析(三十七)

    今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...

  7. solr&lucene3.6.0源码解析(二)

    上文描述了solr3.6.0怎么采用maven管理的方式在eclipse中搭建开发环境,在solr中,为了提高搜索性能,采用了缓存机制,这里描述的是LRU缓存,这里用到了 LinkedHashMap类 ...

  8. solr&lucene3.6.0源码解析(一)

      本文作为系列的第一篇,主要描述的是solr3.6.0开发环境的搭建   首先我们需要从官方网站下载solr的相关文件,下载地址为http://archive.apache.org/dist/luc ...

  9. apache mina2.0源码解析(一)

    apache mina是一个基于java nio的网络通信框架,为TCP UDP ARP等协议提供了一致的编程模型:其源码结构展示了优秀的设计案例,可以为我们的编程事业提供参考. 依照惯例,首先搭建a ...

随机推荐

  1. VSTO不能创建OFFICE 文档项目的原因

    正用的好好的,突然vsto不能用了.我是安装的vs2015 社区版本,本身是不带vsto的,当初不知道怎吗安装上的,昨天突然不能用了.症状是创建excel workbook 类型的项目是失败(创建ad ...

  2. CentOS6.5 服务器+apache5.3绑定多个域名+SELinux设置

    下面简单的介绍了如何通过设置Apache的http.conf文件,进行多个域名以及其相关的二级域名的绑定(假设我们要绑定的域名是minidx.com和ntt.cc,二级域名是blog.minidx.c ...

  3. HDOJ 1217 Floyed Template

    解题思路:1.map简单应用2.Floyd算法的变形,之后判断dis[i][i],如果大于1,则存在利润! #include <iostream> #include <stdio.h ...

  4. Qt国际化相关类(以前没见过codec->toUnicode,QTextCodec,QLocale.toString和QLocale::setDefault,QInputMethod::locale())

    QTextCodec QTextCodec为文本编码之间提供转换. Qt用Unicode 来存储,绘制和操作字符串.在很多情况下你可能希望操作不同编码的数据.例如,大部分日本文档是以Shift-JIS ...

  5. 物理Data Guard主备切换步骤

    物理Data Guard角色转换步骤   Step  1   验证主库是否能执行角色转换到备库(原主库执行) SQL> SELECT SWITCHOVER_STATUS FROM V$DATAB ...

  6. JavaScript对滚动栏的操作

    <html> <head> <meta http-equiv="Content-Type" content="text/html; char ...

  7. IOS系统对fixed定位支持不好的解决方法

    问题: IOS 中所有浏览器,当页面上的输入框获得焦点时,呼出键盘. 页面底部的导航栏(position:fixed)会被键盘顶到页面的中间. 而当输入框失去焦点时,导航栏停留在页面中间,造成页面错乱 ...

  8. 【linux】arm mm内存管理

    欢迎转载,转载时请保留作者信息,谢谢. 邮箱:tangzhongp@163.com 博客园地址:http://www.cnblogs.com/embedded-tzp Csdn博客地址:http:// ...

  9. AspNetCore.Hosting

    Microsoft.AspNetCore.Hosting 有关Hosting的基础知识 Hosting是一个非常重要,但又很难翻译成中文的概念.翻译成:寄宿,大概能勉强地传达它的意思.我们知道,有一些 ...

  10. React.js学习

    React.js学习之环境搭建 1 工欲善其事必先利其器:前端开发工具 1.1 WebStorm和Sublime Text Sublime Text:作为代码编辑器,Sublime Text的优点如下 ...