在上篇博客中分析了hashmap的用法,详情查看java并发之hashmap

本篇博客重点分析下hashmap的源码(基于JDK1.8)

一、成员变量

HashMap有以下主要的成员变量

/**
* The default initial capacity - MUST be a power of two.
默认初始容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30; /**
* The load factor used when none specified in constructor.
默认的加载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 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.
JDK1.8在哈希冲突后,使用链表的方式存储数据,当链表中元素个数超过8个,则转化为红黑树的格式
*/
static final int TREEIFY_THRESHOLD = 8; /**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
当红黑树的节点数少于6个,则转化为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 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; /**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values(). */
transient Set<Map.Entry<K,V>> entrySet; /**
* The number of key-value mappings contained in this map.
key-value的个数
*/
transient int size;

上面对HashMap中的主要成员变量做了注释,重点关注以下几个,

transient Node<K,V>[] table  这个成员变量是HashMap存储键值对的载体,Node类型的数组,可以联想到把键值对封装成了Node对象,然后使用数组存储一个一个的Node,体现了Java三大特性中的封装。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  HashMap的默认容量,即table数据组的默认长度,在构建table数组时使用。

static final int MAXIMUM_CAPACITY = 1 << 30  HashMap的最大容量,即table数据的最大长度。

static final float DEFAULT_LOAD_FACTOR = 0.75f  默认的加载因子,这个变量很重要,关系到HashMap扩容以及数组的饱和程度等

final float loadFactor  加载因子

int threshold  代表HashMap的阈值,=table数组的长度*loadFactor,当HashMap中键值对的数量大于threshold的时候便需要扩容,即把数据的长度扩大一倍

二、构造函数

HashMap提供了以下4个构造函数,

1、HashMap()

这个是默认的构造函数,其实现如下

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

从其实现来看,仅指定了默认的负载因子,其他的均为默认值,默认的负载因子为0.75,这个值是经过经验得出的,是空间和时间上的一个均衡。

2、HashMap(int initialCapacity)

这个可以指定HashMap的初始容量,但此容量并非要创建的Node类型的table的长度,HashMap使用了tableSizeFor(int cap)方法对其处理,此方法下面会说到。构造方法的实现如下,

public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

实现即调用了另一个构造方法

3、HashMap(int initialCapacity, float loadFactor)

这个构造方法可以指定两个参数,一个是初始容量,另一个是负载因子,前面说到容量*负载因子=阀值(threshold),当键值对的数量(size)大于阀值时便要扩容。构造方法实现如下,

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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

对给定的初始容量做了判断,最后通过tableSizeFor函数计算出的值给了threshold。

4、HashMap(Map<? extends K, ? extends V> m)

使用一个Map类型的变量构造HashMap,其实现如下

public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}

指定了负载因子,看起来负载因子很重要。

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

从4个构造方法中,可以看出都并未初始话table变量,即存储数据的数组,那么table变量在什么时候初始化那,是在put方法中。为什么要放在put方法中,那是因为如果我就调用了构造方法,然后初始化了table数组,分配了内存,然而我不向HashMap中放数据,即不调用put方法,那么肯定会造成内存的浪费,所以只有在真正调用put的时候才初始化table,考虑周全呀。

二、工具函数

这里重点分析两个工具函数,hash和tableSizeFor。

1、hash(Object key)

此函数的作用是传递一个key参数,返回一个int数值,

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

如果key为null,则返回0,否则,取key的hashCode值h和h无符号右移16位的异或值。为什么要这样做我们放在后边分析。这个函数决定了每个键值对在table数组中的位置。

2、tableForInt(int cap)

此函数是为了计算大于或等于给定参数的最小的2的N次方。

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;
}

举个例子,现在给一个数19,调用此函数后返回32;给一个数16,调用此函数后返回16,给一个数15,调用此函数后返回16。

三、put/get操作

put和get操作是HashMap中常用的操作,使用频率很高,了解其实现对编写代码很有提升。

1、put(K key, V value)

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

从上面的代码中可以看出调用了putVal方法,使用hash函数对key进行了哈希。putVal的定义如下,

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果HashMap底层的数组table为空,或者其长度位0
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//调用扩容方法进行扩容,并返回扩容后的长度
//如果要存储的key在table中的索引处元素p为null,则说明此key所在索引处未产生hash冲突
if ((p = tab[i = (n - 1) & hash]) == null)
//生成一个Node节点,放在此key的索引处
tab[i] = newNode(hash, key, value, null);
else {//如果此key所在的索引处的元素p不为null,说明已经有其他的key的hash值和现在key的hash相同,产生了hash冲突,两个元素在table中的索引一致
Node<K,V> e; K k;
//如果要插入的key value和p的全部相同,把p赋给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果不相等,判断p的节点类型,如果是TreeNode类型,则证明是红黑树的结构,调用putTreeVal进行元素插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//如果不是TreeNode类型,则说明是链表的结构,使用链表的方式插入,找到链表的尾部,进行插入
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//在p后插入元素
p.next = newNode(hash, key, value, null);
//判断链表的元素数量,如果大于8,则调用treeifyBin方法转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不为null,说明存在一个相同的key,则需要进行value的替换,并返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果插入元素后,元素个数大于threshold(阀值=数组容量*负载因子),进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

在上面的代码中做了详细的注释,下面把过程概括如下

1、判断HashMap底层存储数据的数组table是否为null或者长度为0(这里在进行table的初始化),如果是则进行扩容(第一次叫初始化);

2、如果不是,取出要插入key在数组中索引位置的元素p,判断p是否为null,如果为null,则直接插入;

3、如果p不为null,判断判断p和待插入数据是否相等,如果相等使用e存储p(后面会判断e是否null,如果不为null,则进行值的替换);

4、如果不等,判断p的类型是否为TreeNode,即是否为红黑树的结构,如果是则使用红黑树的方式插入;

5、如果不是TreeNode,则使用链表的方式插入;

6、插入完成后更新元素的个数size,如果size大于threshold进行扩容;

上面是put的大体过程,对于红黑树的插入,暂不做分析,下面分析下扩容函数resize,其源码如下

final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//1、创建一个新的Node数组,保存数据
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//2、如果旧数组不为空,则要把元素拷贝到新数组
if (oldTab != null) {
//循环旧数组中的元素
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//j索引处不为null,取出给e
if ((e = oldTab[j]) != null) {
//清空j处的元素
oldTab[j] = null;
//2.1、判断e是否有后继,如果没有说明仅有一个Node元素,重新计算e中key的hash值,得到在新数组中的索引,进行插入
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//2.2.1判断e是否为TreeNode类型,如果是使用红黑树的方式
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//2.2.2不是红黑树的数据结构,为链表结构,进行链表结构的数据拷贝
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
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;
}
}
}
}
}
return newTab;
}

在上面源码中进行了详细注释,具体步骤可查看注释。

2、get(Object key)

get函数是使用key取出其对于的value的过程,其源码如下

public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

使用getNode函数取出Node元素,如果Node为null,则返回null,如果不为则返回其value属性值,关于Node类的构成稍后分析,先看getNode函数,

final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//1、判断table不为null且长度大于0,且要取的key处索引位置元素不为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//2、如果第一个元素和给定的key相等则直接返回第一个元素first
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//3.1、如果第一个元素的类型为TreeNode,使用红黑树的方式取得Node
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//3.2、使用链表的方式取得Node
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}

在上面的代码中进行了详细注释,可参考。

下面看下Node的结构,

static class Node<K,V> implements Map.Entry<K,V> {
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作为HashMap的静态内部类,其属性有hash、key、value、next,使用这些属性存储数据,其中key value即为我们说的hashMap中的键值对,这里使用Node进行封装。next指向下个Node的地址。

以上对HashMap做了主要分析,后面计划对其哈希hash函数即红黑树做分析。

有不正之处,欢迎指正!

java并发之hashmap源码的更多相关文章

  1. Java HashSet和HashMap源码剖析

    转自: Java HashSet和HashMap源码剖析 总体介绍 之所以把HashSet和HashMap放在一起讲解,是因为二者在Java里有着相同的实现,前者仅仅是对后者做了一层包装,也就是说Ha ...

  2. 【转】Java集合:HashMap源码剖析

    Java集合:HashMap源码剖析   一.HashMap概述二.HashMap的数据结构三.HashMap源码分析     1.关键属性     2.构造方法     3.存储数据     4.调 ...

  3. 死磕Java之聊聊HashMap源码(基于JDK1.8)

    死磕Java之聊聊HashMap源码(基于JDK1.8) http://cmsblogs.com/?p=4731 为什么面试要问hashmap 的原理

  4. 【JAVA集合】HashMap源码分析(转载)

    原文出处:http://www.cnblogs.com/chenpi/p/5280304.html 以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储 ...

  5. Java集合:HashMap源码剖析

    一.HashMap概述 HashMap基于哈希表的 Map 接口的实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.(除了不同步和允许使用 null 之外,HashMap  ...

  6. Java中的HashMap源码记录以及并发环境的几个问题

    HashMap源码简单分析: 1 一切需要从HashMap属性字段说起: /** The default initial capacity - MUST be a power of two. 初始容量 ...

  7. Java集合之HashMap源码实现分析

    1.简介 通过上面的一篇随笔我们知道了HashSet的底层是采用Map实现的,那么Map是什么?它的底层又是如何实现的呢?这下我们来分析下源码,看看具体的结构与实现.Map 集合类用于存储元素对(称作 ...

  8. 死磕 java集合之HashMap源码分析

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 简介 HashMap采用key/value存储结构,每个key对应唯一的value,查询和修改 ...

  9. java集合之HashMap源码解析

    Map是java中的一种数据结构,围绕着Map接口,有一系列的实现类如Hashtable.HashMap.LinkedHashMap和TreeMap.而其中HashMap和Hashtable我们平常使 ...

随机推荐

  1. c# 输出不同时间的格式

    C#时间/日期格式大全,C#时间/日期函数大全 有时候我们要对时间进行转换,达到不同的显示效果 默认格式为:2005-6-6 14:33:34 如果要换成成200506,06-2005,2005-6- ...

  2. pandas处理较大数据量级的方法 - chunk,hdf,pkl

    前情提要: 工作原因需要处理一批约30G左右的CSV数据,数据量级不需要hadoop的使用,同时由于办公的本本内存较低的缘故,需要解读取数据时内存不足的原因. 操作流程: 方法与方式:首先是读取数据, ...

  3. MongoDB之Replica Sets环境搭建

    最近学习MongoDB,这两天在搭建复制集的时候碰到了不少问题,也踩了好多坑,现在在这里记录下来,以供自己和他人参考 (因为本人是初学者,所以遇到的问题也会比较初级,所以本文也比较适合初学者查阅) 背 ...

  4. zoj 4056

    At 0 second, the LED light is initially off. After BaoBao presses the button 2 times, the LED light ...

  5. Fiddler证书安装不成功

    Fiddler 抓包https配置 提示creation of the root certificate was not successful 证书安装不成功 原文链接 在使用Fiddler抓包时,我 ...

  6. CSS(非布局样式)

    CSS(非布局样式) 问题1.CSS样式(选择器)的优先级 1.计算权重 2.!important 3.内联样式比外嵌样式高 4.后写的优先级高 问题2.雪碧图的作用 1.减少 HTTP 请求数,提高 ...

  7. hibernate源码分析1-保存一个对象

    要点 用了event的方式贯穿CRUD的过程 值得学习 用dynamic-insert 支持 插入时 可选 全字段插入 还是仅仅有值的字段插入. 返回主键的值 用了 Serializable 类型作为 ...

  8. Just a test

  9. ant 入门级详解

    ant 入门级详解   [转载的地址(也是转载,未找到原文地址)]https://www.cnblogs.com/jsfx/p/6233645.html 1,什么是antant是构建工具2,什么是构建 ...

  10. 思路清奇:通过 JavaScript 获取移动设备的型号

    我们一般在浏览器里识别用户的访问设备都是通过 User Agent 这个字段来获取的,但是通过它我们只能获取一个大概的信息,比如你用的是 Mac 还是 Windows,用的是 iPhone 还是 iP ...