OpenJDK1.8.0 源码解析————HashMap的实现(一)
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接口里的一个内部接口)
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
}
再看Node的源码(Node是HashMap类的一个静态内部类,实现了Map接口里的内部接口Entry)
static class Node<K,V> implements Map.Entry<K,V> {
//这四个成员变量就是一个Node节点所包含的四个变量域
//其中hash的计算方法是由一个hash方法得到的,该方法的是实现就是HashMap里,这里为了看的清楚一些,就写到下面:
// hash = (h = key.hashCode()) ^ (h >>> 16);
final int hash;
final K key;
V value;
Node<K,V> next;
//构造方法
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//拿到该Node 的key
public final K getKey() { return key; }
//拿到该Node 的value
public final V getValue() { return value; }
//重写toString方法
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里实现了很多关于红黑树的操作方法,如果全部放到这里不仅看不懂而且显得特别乱)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
//构造函数
TreeNode(int hash, K key, V val, Node<K,V> next){
super(hash, key, val, next);
} //返回该树的树根
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;){
if ((p = r.parent) == null) return r; r = p;
}
}
//其余部分省略,有兴趣的可以自行查看源代码。 }
然后在说一下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里几个重要的成员变量和常量(省略了一部分)。
//通过HahMap实现的接口可知,其支持所有映射操作,能被克隆,支持序列化
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; //默认Node<K,V> table的初始容量16,2的4次方。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //table数组的最大容量为1073 741 824,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30; /*
默认加载因子
该值和table的长度的乘积为是否对table进行扩容的标志。
例如当该值默认为0.75,table的长度为16时,就是说当table填充至 12*0.75 =12 之后,这个table就要进行扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; //哈希表的定义,这个数据结构在前面已经介绍过
transient Node<K,V>[] table; /*
HashMap的大小,即保存的键值对的数量
当该值大于等于HashMap的阈值时,数组就会扩充
*/
transient int size; /*
HashMap的阈值.
用于判断是否需要调整HashMap的容量
该值的计算方法为: 加载因子 * (table.length)
当table.size>=threshold就会扩容,就是如果hashMap中存放的键值对大于这个阈值,就进行扩容
如果加载因子没有被分配,默认为0.75
如果table数组没有被分配,默认为初始容量值(16);
或若threshold值为0,也表明该值为初始容量值。
*/
int threshold; /*
加载因子
如果加载因子没有被分配,默认为0.75
*/
final float loadFactor;
//当添加到tab[i]位置下的链表长度达到8时将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8; ......
}
介绍完数据结构再来说一下我们平时经常写的代码:Map<String,Object> map = new HashMap<String,Object>()
(这样的方式获得的map对象,调用的是默认无参的构造,实际上还有其他三个有参的构造函数)
要知道写完这句代码之后到底发生了什么。我们首先就得看一下HashMap构造器(共4个构造器),源码如下
/*
默认无参构
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
} //构造一个带指定初始容量和加载因子的空 HashMap。
public HashMap(int initialCapacity, float loadFactor) {
//初始容量为负数,抛出异常。
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//初始容量大于最大容量,把初始容量定为最大容量。
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果加载因子不符合规范,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
//经过一些过滤和处理,现在的loadFactor和initialCapacity都为合法的值
//初始化加载因子
this.loadFactor = loadFactor;
/*
初始化HashMap的阈值
调用tableSizeFor(initialCapacity)方法
该方法根据数组的初始容量的大小求出HashMapde的阈值threshold
*/
this.threshold = tableSizeFor(initialCapacity);
} /*
第三个构造方法
构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
} // 构造一个映射关系与指定 Map 相同的新 HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
上面提到了根据数组初始容量得出HashMap阈值的一个方法:tableSizeFor(int cap)。该方法的源码如下:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
简单的测试了一下这个方法
public class TestHashMap{
static final int MAXIMUM_CAPACITY = 1 << 30;
public static void main(String[] args){ System.out.println("---------------------");
System.out.println(" 第一次测试,cap = 0,返回的阈值threshold = "+tableSizeFor(0));
System.out.println("---------------------");
System.out.println(" 第二次测试,cap = 3,返回的阈值threshold = "+tableSizeFor(3));
System.out.println("---------------------");
System.out.println(" 第三次测试,cap = 16,返回的阈值threshold = "+tableSizeFor(16));
System.out.println("---------------------");
System.out.println(" 第四次测试,cap = 100,返回的阈值threshold = "+tableSizeFor(100));
System.out.println("---------------------");
System.out.println(" 第五次测试,cap = 1000,返回的阈值threshold = "+tableSizeFor(1000));
System.out.println("---------------------");
System.out.println(" 第六次测试,cap = 10000,返回的阈值threshold = "+tableSizeFor(10000));
System.out.println("---------------------"); } static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
}
输出结果:
---------------------
第一次测试,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一部分介绍完了。哪里有不对的地方,还望指出。还有就是如果能解答我的疑惑,还请指导,十分感谢 ^_^。
OpenJDK1.8.0 源码解析————HashMap的实现(一)的更多相关文章
- OpenJDK1.8.0 源码解析————HashMap的实现(二)
上一篇文章介绍了HashMap的一部分的知识,算是为下面HashMap的进一步学习做准备吧. 然后写的时候一直在思考的一个问题是,这方面的知识网上的资料也是一抓一大把,即使是这样我为什么还要花费时间去 ...
- solr&lucene3.6.0源码解析(四)
本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...
- Android事件总线(二)EventBus3.0源码解析
1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus ...
- JDK8源码解析 -- HashMap(二)
在上一篇JDK8源码解析 -- HashMap(一)的博客中关于HashMap的重要知识点已经讲了差不多了,还有一些内容我会在今天这篇博客中说说,同时我也会把一些我不懂的问题抛出来,希望看到我这篇博客 ...
- solr&lucene3.6.0源码解析(三)
solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...
- Heritrix 3.1.0 源码解析(三十七)
今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...
- solr&lucene3.6.0源码解析(二)
上文描述了solr3.6.0怎么采用maven管理的方式在eclipse中搭建开发环境,在solr中,为了提高搜索性能,采用了缓存机制,这里描述的是LRU缓存,这里用到了 LinkedHashMap类 ...
- solr&lucene3.6.0源码解析(一)
本文作为系列的第一篇,主要描述的是solr3.6.0开发环境的搭建 首先我们需要从官方网站下载solr的相关文件,下载地址为http://archive.apache.org/dist/luc ...
- apache mina2.0源码解析(一)
apache mina是一个基于java nio的网络通信框架,为TCP UDP ARP等协议提供了一致的编程模型:其源码结构展示了优秀的设计案例,可以为我们的编程事业提供参考. 依照惯例,首先搭建a ...
随机推荐
- 高级UIKit-01(总结基础UIKit)
总结: 如果相同的控件大于等于3个就拖成一个属性选用outlet Collection 提升局部变量的方法:传参或改变全局 创建CGImage对象要释放,因为ARC只会自动释放OC方法,这个是CG框架 ...
- shell登录模式及其相应配置文件(转)
参考<linux命令.编辑器与shell编程>(清华大学出版社) 当启动shell时,它将运行启动文件来初始化自己.具体运行哪个文件取决于该shell是登陆shell还是非登陆shell的 ...
- NGUI 3.5教程(二)Label 标签 (Hello world)、多行文本
写在前面: 本文将创建NGUI的第一个样例.依照编程传统,第一个样例,就是做一个Hello world 显示出来.NGUI.我们用Label来实现 . 欢迎大家纠错.拍砖!原创非常辛苦,如有转 ...
- CSS实现强制换行-------Day 78
事实上最早的时候也考虑过这个问题,当时还在想须要判定文字的长度么,实在是傻到极点了,原来CSS中本来就有这个样式设置的.而今天正好看到了有这么一篇介绍.细致看了下,感觉还不错,这里也把实验的结果记录下 ...
- javascript笔记整理(对象的继承顺序、分类)
Object.prototype.say=function(){ alert("我是顶层的方法"); } children.prototype=new parent(); pare ...
- javascript笔记整理(数据类型强制/隐式转换 )
A.数据类型强制转换 1.转换为数值类型 Number(参数) 把任何的类型转换为数值类型 A.如果是布尔值,false为0,true为1 var a=false;alert(Number(a)); ...
- 基于visual Studio2013解决C语言竞赛题之1024求和
题目 解决代码及点评 /* 已知有N个无规律的正整数,请编程序求出其中的素数并打印出能被5整除的数之积. */ #include <stdio.h> # ...
- mysql双机热备的配置步骤
设置双机热备: 首先要在两台机器上建立同步用户: grant replication slave on *.* to 'repdcs'@'192.168.21.39' identified by '1 ...
- Selenium 出现: Caused by: java.lang.ClassNotFoundException: org.w3c.dom.ElementTraversal
webDriver 运行的时候出现: Caused by: java.lang.ClassNotFoundException: org.w3c.dom.ElementTraversal 解决办法: 只 ...
- 施用 maven shade plugin 解决 jar 或类的多版本冲突
施用 maven shade plugin 解决 jar 或类的多版本冲突 使用 maven shade plugin 解决 jar 或类的多版本冲突java 应用经常会碰到的依赖的三方库出现版本 ...