java深入学习Hashmap源码(JDK8)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dongensi/article/details/84813920
关于JDK1.6、1.7、1.8三个版本,HaspMap的实现是有区别的,特别是1.8,对hashmap的结构进行了较大的变化。

1.6整体采用的是位桶+链表的方式,而1.8使用的是位桶+链表+红黑树实现,链表的阈值是8,超过后就由链表变成红黑树,大大增加了查询的效率。


[img]http://dl2.iteye.com/upload/attachment/0119/4417/ef91aa10-43fc-3066-8368-f68d30946976.png[/img]


[size=large]1 涉及到的数据结构:处理hash冲突的链表和红黑树以及位桶[/size]

    //Node是单向链表,它实现了Map.Entry接口
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
//构造函数Hash值 键 值 下一个节点
Node(int hash, K key, V value, Node 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;
}
//判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true
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;
}
}

//红黑树
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // 父节点
TreeNode left; //左子树
TreeNode right;//右子树
TreeNode prev; // needed to unlink next upon deletion
boolean red; //颜色属性
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}

//返回当前节点的根节点
final TreeNode root() {
for (TreeNode r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}

transient Node[] table;//存储(位桶)的数组


以上3个数据结构,可以大致联想到HashMap的实现。首先有一个每个元素都是链表的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,所以说数组存放的是链表。而当链表长度超过8时,链表就转换为红黑树。


[size=large]2.构造函数[/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);
}


Map中,threshold = initialCapacity * loadFactor;所以在这里面,loadFactor越小,占用的空间就越多,查询检索的效率就越高;反之loadFactor越大,占用空间就小,而查询检索效率就低。由于haspMap的定义就是空间换时间,loadFactor负载因子值设置非常重要。

默认情况下,loadFactor为0.75,threshold 为12;如果loadFactor为1,threshold 就是16,为什么说loadFactor越大,空间占用小?比如一个为15的Map放入,那么在loadFactor在0.75时,是需要扩容的,threshold 就为24,此时初始空间为32;而为1的情况下不需要扩容
,所以占用空间就小。扩容后,位桶table也会增大,为旧的两倍,对比不扩容,位桶上hash冲突会减少很多,检索查询效率自然会高些。


[size=large]3、扩容机制[/size]

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length),就会进行扩容,由于扩容存在数组复制,扩容是比较耗费时间的。代码

  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; //扩容值翻倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 未设定使用默认值
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<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表时,需要复制新的
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;
}


[size=large]4、put/get时Node[] table位置[/size]

由于都是通过hash确定代码,

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

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

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)
n = (tab = resize()).length; //table为空的时候,n为table的长度
if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash 与1.6中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。
tab[i] = newNode(hash, key, value, null);
else {
// 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//相同则覆盖之
else if (p instanceof TreeNode)
// 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
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) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

整体过程:
1、判断键值对数组tab[]是否为空或为null,否则resize();
2、根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
3、判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理。
4、普通中关键代码 (n - 1) & hash; hash先由key值通过hash(key)获取,再通过 hash &(n即为length-1)得到所在数组位置。一般对于哈希表的散列常用的方法有直接定址法,除留余数法等,既要便于计算,又能减少冲突,但是取模中的除法运算效率很低,所以HashMap则通过hash &(length-1)。

为什么要减1?如图

[img]http://dl2.iteye.com/upload/attachment/0119/4419/853332b4-82d5-3cb1-841a-78be25a25e64.png[/img]

一是保证使用 和运算时能够最大的在位桶上均分hash值,减少hash冲突,上图在未-1情况下,都只有一种结果;数组的length永远是2的N次方,那么length-1最后转成2进制,最后一位都是1,& 操作就可能出现0 或 1,如果不减1,那么2进制最后一位永远都是0,& 的运算结果只有0,明显增加了hash的冲突;
二是保证了结果都在length的长度内,& 操作后,结果永远都是 <= length 或者length -1,但在极端情况下出现 Node[length],就会出现问题,而Node[length -1]不会。

在HashMap实现中还可以看到如下代码取代了以前版本JDK1.6的while循环来保证哈希表的容量一直是2的整数倍数,用移位操作取代了循环移位。


    //这段代码保证HashMap的容量总是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;
}



参考:

http://www.2cto.com/kf/201505/401433.html
http://www.cnblogs.com/hfczgo/p/4033283.html?utm_source=tuicool&utm_medium=referral
展开阅读全文

没有更多推荐了,返回首页