您的位置:首页 > 编程语言 > Java开发

JDK1.8源码解析之 HashMap

2016-10-13 17:50 671 查看

JDK1.8源码解析之 HashMap

目录

HashMap 基本用法

HashMap 源码分析

总结

一.HashMap 基本用法

HashMap主要有以下几种操作:


public boolean containsKey(Object key)
public boolean containsValue(Object value)
public V put(K key, V value)
public V get(Object key)
public Set<K> keySet()
public V remove(Object key)


二.HashMap 源码分析

首先看一下hashmap的类层次关系:



public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable


它包含了四种构造函数,其中capacity,loadfactor可以自己设定也可以使用默认值

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

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

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

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


HashMap的源码比较长,但主要的有以下几点

关键的两个变量

所使用到的数据结构

put(K,V)操作

getkey(K)

resize()

hash()

(1)主要成员变量

CAPACITY:容量

LOAD_FACTOR:负载因子

/**
* The default initial capacity - MUST be a power of two.
* 默认的容量,必须是2的次幂,要求HashMap底层实现数组的长度为2的幂,原因是可以得到较好
* 的散列性能。在构造函数中如果自定义capacity且不是2 的次幂,程序会根据所给定
* 的capacity找最接近的2的幂来作为容器的初始容量,这有利于改善hash算法。aka 16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
* 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.
* 树的门阀值,即当链表的长度超过这个值的时候,链表将被转化成红黑树结构
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* 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.)
* 主要的存储结构(包含两种:链表结构,红黑树结构)TreeNode是Node子类
*/
transient Node<K,V>[] table;

/**
* The next size value at which to resize (capacity * loadfactor).
* 相当于阈值,当填充的数量大于这个值的时候将会进行resize()操作
* @serial
*/
int threshold;

/**
* The load factor for the hash table.
* 可使用默认值,也可以自己设定,但是从一些数据来看,负载因子设置为0。75已经足够,
* 若继续扩大,所带来的益处并不是特别明显
* @serial
*/
final float loadFactor;


(2)所使用到的数据结构(重大变化)

-



jdk1.8版本可以说是近几次版本更新中变化最大的一次,比如引入lambda表达式等等,在集合类这一块,hashmap变化是非常大的。在jdk1.7版本中,HashMap中是使用以“数组+链表”的基本结构来存储key和value构成的Entry单元的。其中链表结构是用来解决冲突的,这种结构固然简单,但是如果,发生冲突次数非常之多的话,那么链表的长度会非常长,而对于链表结构的基本操作的时间复杂度基本上是O(n),所以,过长的链表会影响性能。在jdk 1.8中是使用以“数组+链表+红黑树”的基本结构来存储key和value构成的Entry单元的,其中红黑树,与链表共同协作解决冲突问题,当链表长度大于8的时候,链表会被转化成红黑树结构,时间复杂度也就随之下降为O(log n),而当树的大小小于6的时候将会转化成链表结构,虽然这些转化会消耗一定的时间,但与其所带来的性能上的提高,还是显得微不足道。

下面是结点的源码:

/**
* Basic hash bin node, used for most entries.  (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
* 最基本的容器结点
*/
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;
}

public final K getKey()        { return key; }
public final V getValue()      { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

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


(3)关键的几个函数源码分析

hash()

put()

resize()

getkey()

hash()操作

根据key值来计算hash值

/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower.  Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.)  So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


下面是计算一个数组下标的步骤

1.根据key值计算出对应的hashcode

2.高16位与低16位相异或得出hashmap中对应的hash值

3.用当前数组的容量大小n 与上面得出的hash值进行与运算即可得出下标 这一步将会在put()函数中用到


%20-%20SIMPLE1995%E7%9A%84%E5%8D%9A%E5%AE%A2%20-%20%E5%8D%9A%E5%AE%A2%E9%A2%91%E9%81%93%20-%20CSDN.NET_files/20160703165857217)

put()操作

put函数用法:put(key,value),即向map中填入一个键值对

/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
*         <tt>null</tt> if there was no mapping for <tt>key</tt>.
*         (A <tt>null</tt> return can also indicate that the map
*         previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}


从上面的源码我们可以看到,put操作实际上是调用了putVal(K,V)操作,下面我们将重点讲述putVal()函数

putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)主要有以下几个步骤:

检查table[]是否为空,若为空,则进行初始化(resize)

根据key值计算下标p,方法在hash()函数中已经讲解,第一个形参即hash值是采用hash()计算得出,之所以使用次函数而不使用object.hashcode()是因为此函数计算出的hash值会尽量的减少冲突,也就尽可能避免过多冲突的问题

若table[p]为空,说明在当前桶中并没有任何put操作,直接创建一个节点即可;否则,进行第4步;

不为空,即产生冲突,之后就要遍历下面的链表或者红黑树,如果table[p]是treeNode节点说明此桶中是红黑树结构(若是,则直接转到putTreeVal()函数进行操作),否则是链表结构;在遍历过程中如果发现有相同的hash值的时候,说明此前map中已经存在一个以key为键的键值对,这时只需要用当前的值替换原来的值即可;否则,进行第5步;

进行到这一步,说明在map中没有与其重复的key,假设当前是链表结构,如果插入一个键值对后当前桶的大小大于等于红黑树转换的阈值(即TREEIFY_THRESHOLD),那么将进行链表结构向红黑树结构的转变(treeifyBin(table, hash)实现转变,参数中table是哈希表,hash是hash()函数计算传来的值,可用来计算下标,根据这两个参数便可观察到当前桶中的所有节点的信息);否则,直接在链表后面插入即可;

完成第5步操作后,会进行最后一步判断,所有节点数量总和(即size)与threshold(capacity * loadfactor)之间的大小比较,如果size > threshold,就会进行扩容,在下一节会讲解resize()函数。

/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//步骤1
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//步骤2 计算下标 并判断
tab[i] = newNode(hash, key, value, null);//步骤3 当前桶为空
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //步骤4 第一个值与key相同,直接替换旧值
e = p;
else if (p instanceof TreeNode)//第一个值是treenode,说明是红黑树结构
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//步骤5 没有找到相同的key
p.next = newNode(hash, key, value, null);
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;
}
}
if (e != null) { // 步骤4 existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)//步骤6 扩容
resize();
afterNodeInsertion(evict);
return null;
}


resize()函数

实现扩容功能(2倍),增大table[]的大小;这个函数还是比较简单的,先创建一个新的数组(容量为原来的2倍),然后原来数组中的元素要么仍然在原来的桶中,要么就转移到下标为:当前下标(j) +原来的容量(oldCap) 的位置,至于是否移动是利用(e.hash & oldCap)值来判断。

/**
* Initializes or doubles table size.  If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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 2倍
}
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;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//如果遇到红黑树结构,就转换的treenode中的split函数实现
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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 {//新位置(原来下标+oldcapacity)
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;
}


当然了,没有图片,怎么能做到浅显易懂,从网上找了好久才找到一些关于hashmap resize部分的图片图片,下面就看看吧:

我们来看看这张图,原来桶的大小为16,扩容后为32,二进制表示中就相当于增加了一位,然后再重新与运算计算下标即可。





第三张图我认为比第一张图要标准许多,我们一定要注意哈希桶数组中的每一个节点都是其桶中元素的第一个节点。由于在桶中的每个hash值大小都是随机的,根据概率我们可以认为是均匀划分的。



下面这张图是jdk1.7版本的扩容图,在扩容之前,哈希桶数组大小为2,因为是2 倍扩容,所以扩容后的大小为4 ,下面就要进行桶中元素的移动了。和上面那张图相比较,我们会发现,1.7版本中移动到新位置的链表是倒置的,而1.8版本并没有这种情况,这也算是1.8版本的一个特点吧。



总结

1. jdk1.8 中HashMap在性能上有很大的提升,因为引入红黑树结构可以有效解决链表过长的问题

2. HashMap 是非线程安全的,如果牵扯到线程问题,我们可以自己加锁,或者使用线程安全类ConcurrentHashMap

3. 以上分析主要还是对链表结构的分析,由于对红黑树结构的知识还是比较多的,而且其插入删除所考虑的问题远多于链表结构,所以要完全讲解也要好几倍以上的篇幅,待以后有时间我在具体解释一下红黑树的特点和主要操作,敬请期待。

参考文章:

重新认识HashMap(in JDK1.8)

浅析 jdk1.8源码之HashMap
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jdk 源码 hashmap