集合源码学习(七):HashMap(Java8)
来源:互联网 发布:淘宝下单后商品下架了 编辑:程序博客网 时间:2024/06/16 13:43
Java8中,新加了很多新特性,特别是集合,分割迭代器,Stream,Functional Interface等等,Java8中的HashMap也和以往的实现略有不同。
这些天看了好久的HashMap,理清了HashMap的结构以及实现原理,听我慢慢分析。
HashMap是什么?
/** * 基于Map接口实现,允许null值和null键。 * HashMap和HashTable很相似,只是HashTable是同步的,以及不能为null的键 * HashMap有两个重要参数,capacity和load factor 默认的load factor大小为0.75 * iterator是fail-fast的。 * */public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
如上,基本的特性在代码里面注释了,HashMap实现了Map接口,是一个基于散列表的Map类,Map接口的特性就是存储键值对。散列表是一种存储结构,它可以通过散列函数直接访问到目标数据值,所以在定位下标方面可认为为o(1)。
HashMap重要的字段
/** * Hash的默认大小 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * HashMap最大存储容量 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 增长因子,意思就是当table已经用到table.length*0.75时,就需要扩容 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 由链表存储转变为由树存储的门限,最少是8 */ static final int TREEIFY_THRESHOLD = 8; /** * 由树存储节点转化为树的节点,默认是6,即从8到6时,重新转化为链表存储 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 当由链表转为树时候,此时Hash表的最小容量。 也就是如果没有到64的话,就会进行resize的扩容操作。 * 这个值最小要是TREEIFY_THRESHOLD的4倍。 */ static final int MIN_TREEIFY_CAPACITY = 64;
上述代码中解释了HashMap中重要字段的意思,相信大家一看就会有大概理解了。
由于在Java8的实现中,当经过hash函数计算得出的下标地址冲突到一定范围时,就会 把冲突的数据用链表的形式连起来,而当用链表数据大于一定范围时,就会将链表转化为红黑树存储。
关于链表,Java中典型应用就是LinkedList,可以看:LinkedList
而红黑树,Java中典型应用是TreeMap, 可以看:TreeMap
HashMap结构概括
首先HashMap会有一个基准数组table:
/** * 存储数据的table集合,长度一定为2的倍数 */ transient Node<K, V>[] table;
第一步,table是一个数组,所以会有下标,HashMap首先会根据传入每个节点的(key,value)中的key,算出应该放到哪一个下标的数组中。
第二步,如果此下标数组为null,那么就直接放入,不为null,就走到第三步。
第三步,如果不为null,就说明冲突了,检查key的equals方法,看是否和原节点的key相同,相同就直接替换,否则进入第四步。
第四步,很明显冲突了,而且是不相等的冲突,这是检查是否需要将此下标的存储结构换为红黑树,不需要就是链表直接在末尾插入节点,否则进入第五步。
第五步,原有的链表结构不足以支撑存储了,所以换为红黑树存储了,此时就是往红黑树中插入该节点。
上述步骤省略了链表与红黑树之间转换。
整个存储结构图如下(没有放入红黑树存储结构)(省略了value值)
HashMap的存储节点
首先看HashMap的Node节点代码,这就是table数组所使用的结构。如果冲突的是链表存储,则直接是这种结构存储。
static class Node<K, V> implements Map.Entry<K, V> { final int hash; final K key; V value; // 可能要连接下面的链表,所以会有个next Node<K, V> next; ... 省略
再看红黑二叉树存储的结构:
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> { TreeNode<K, V> parent; // red-black tree links,红黑树,保证是一棵平衡二叉树 TreeNode<K, V> left; //左子树 TreeNode<K, V> right; //右子树 TreeNode<K, V> prev; // 指向下一个节点,类似于线索二叉树, needed to unlink next upon // deletion,删除时记得置null boolean red; //红黑特性 ... 省略
这是当不用链表表示冲突值时候,用红黑树表示时候的节点。由上可知,TreeNode继承自LinkedHashMap.Entry,
而它的结构如下:
static class Entry<K,V> extends HashMap.Node<K,V>
所以,其实TreeNode是Node的一个子类,所以table中也是可以存放TreeNode的。
hash值的计算方法
这里首先介绍hash值的计算方法,这也是一门有学问有艺术性的东西。
在HashMap中要注意区分hashCode和hash两个方法,他们是不通的!!
这里就不细说hashCode了,看下面hash方法
/** * 自己低位和高位异或操作,能够降低冲突 计算冲突,结合高16位与低16位 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
hashCode()是一个native方法,意味着方法的实现和硬件平台有关,默认实现和虚拟机有关,对于有些JVM,hashCode()返回的就是对象的地址,大多时候JVM根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,并返回。
所以hashCode返回的是一个32位的2进制数值,而Java8中这样的实现,保证了对象的hashCode的高16位的变化能反应到低16位中,相比较而言减少了过多的位运算,是一种折中的设计。
table的容量只能是2的倍数
table容量为2的倍数时,有利于下一个缓解的计算table的下标,另一个方面,虽然在HashMap中,提供了一个构造方法:
public HashMap(int initialCapacity, float loadFactor)
看似提供了初始容量的方法,但是这个方法最后一行代码中调用了另一个方法tableSizeFor
来确定table的容量:
/** * hashMap大小只能为map的倍数。 最终会返回一个最适合cap的2的倍数 * capacity. */ 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; }
所以最终table的length只能是2的倍数。
table下标计算方法
有点基础的读者应该知道,一般索引,都是传入的是一个键(key),而这里的key是一个引用类型,通俗点,就是个类(class),既然是个class,那又怎么获取下标呢?即怎么与下标联系起来呢?看下面代码:
tab[(n - 1) & hash]
没错,就是通过这样的方式,其中hash=hash(key),n=table.length
。这行端代码就相当有艺术了。
由前面知道,table.length是一个2的倍数,随意化成2进制就是开头一个1,后面n个0。随意当减1后,就会变成一排1,
之后,在与刚刚得到的hash(通过高位和低位计算后得到的hash)值做二进制与操作,因为(n-1)的高位都是0,所以最终只会截
取到hash的后log(n)-1
位,会得到一个范围在0~table.length的值,这个值,就是数组的下标。是不是很有艺术。
由于hash是由key的hashCode的高16位与低16位经过异或而得,混合了原始哈希码的高低位,大大的提升了随机性,也让碰撞机率大大降低。
put方法
前面已经讲了put方法的基本过程,下面再细看看putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
,其中,如果key=null,那么hash(key)=0,所以是能够存放null值的。方法实现代码:
/** * 插入值, onlyIfAbsent,为真的话,就是不替换,无就插,有就不插 Implements Map.put and related * methods evict,表示需要调整二叉树结构,LinkedHashMap中需要 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K, V>[] tab; //存放table Node<K, V> p; //存放以前存放在table[(n-1)&hash]的节点,如果有 int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //判断是否需要扩容 if ((p = tab[i = (n - 1) & hash]) == null) // 没有数据,就是放一个链表头节点 tab[i] = newNode(hash, key, value, null); else { Node<K, V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 一模一样,连key也equals后相等时 e = p; else if (p instanceof TreeNode) // 判断是二叉树 e = ((TreeNode<K, V>) p) .putTreeVal(this, tab, hash, key, value); else { // 那么就是链表放链表,链尾 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; //其中,如果key的equals也相等,就直接替换 p = e; } } // 替换操作,key一样,旧值换为新值 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(); //LinkedHashMap使用 afterNodeInsertion(evict); return null; }
具体代码分析已经注释到了代码里面。
get方法
如下代码:
/** * 根据key返回它的值。 */ public V get(Object key) { Node<K, V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
如上代码所示,可以获取null值。
/** * 根据key返回值。 也就是先算hash,在找到其位置,在看是否有因冲突而产生的链表或者二叉树。 */ final Node<K, V> getNode(int hash, Object key) { Node<K, V>[] tab; //指向table,这样如果对table加锁,自己还是能够只读的 Node<K, V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // 总是检查是否为头节点。 ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) // 二叉树 return ((TreeNode<K, V>) first).getTreeNode(hash, key); do { // 链表 if (e.hash == hash && ((k = e.key) == key || (key != null && key .equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
基本的由key获取节点的过程大致是这样,代码中已有注释,这里就不多讲。
remove方法
如下代码:
/** * 根据key,删掉这个节点。 */ public V remove(Object key) { Node<K, V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
接下来看具体的removeNode方法:
/** * 删除某一个节点。 * @param matchValue * 如果为真,那么只有当value也想等时,才能删除。 * @param movable 能否删除 */ final Node<K, V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K, V>[] tab; Node<K, V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { //寻找node节点过程 Node<K, V> node = null, e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K, V>) p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key .equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //node节点就是已经找到的,符合条件的要删除的节点。 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value .equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
具体代码和前面的get方法相似,先找到节点,然后在判断哪种方法删除,以及删除之后的调整。
containsValue方法
和containsKey方法不同,它可以通过先散列,在判断key是否equals来判断是否含有这个key,而containsValue方法,则是直接暴力枚举所有value,然后得出有这个value,性能较差。
/** * 在map中如果至少有一个value的值为value,就返回true。 ,注意下面有个双重循环,一个是循环数组,一个是循环链表(二叉树)。 */ public boolean containsValue(Object value) { Node<K, V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K, V> e = tab[i]; e != null; e = e.next) { //在TreeNode中,next属性也够用,因为TreeNode的父类是Node if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }
resize方法
前面讲过,当table的使用量到达length*loadFactor时,机会触发扩容操作,扩容操作的基本流程为:
1、判断是否需要扩容
2、将老数组table的元素,一个一个遍历并插入到新数组newTable中
3、更改相应的字段属性值。
/** * 初始化使用, * 或者将hashmap大小调整为2的倍数级使用。 */ 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) { // 如果当前size大于最大容量,则下一次就是int的最大值 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; // 把旧数组,复制到新数组。 @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) // 当这个位置没有东西时候,就直接取莫放在这里。,重新计算hash值以便。 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) // 是二叉树节点 ((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 { 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里面的iterator
HashMap里面具有下面几种Iterator:
HashIterator:普通Iterator的父类
KeyIterator:key的Iterator,继承自HashIterator
ValueIterator:Value的Iterator,继承自HashIterator
EntryIterator:key和value的Iterator,继承自HashIterator
同样的,HashMap里面也有Spliterator:
关于何为Spliterator,请看这篇: 集合源码学习:Spliterator
但是就目前Java8的源码来看,HashMap里面的分割列表,它是基于table的元素进行迭代的,啥意思呢?
在就是在trySplit方法里面,仅仅是对table进行横向的分割,类似于对数组的分割。
而在tryAdvance中,只会对table[current]进行以下 的遍历,即遍历链表或二叉树。如果current为null,则向下找一个不为空的table[current],找到后,只遍历一个table[current]找的终点则是本Spliterator的fence。
而在forEachRemaining中,遍历多个,和tryAdvance不同的时,它会遍历本Spliterator所有不为null的table[current]。
学习过程中,从很多文章中学到了知识:
http://www.importnew.com/20121.html
https://www.zhihu.com/question/20733617/answer/111577937
http://www.cnblogs.com/tonyluis/p/5671873.html
http://blog.csdn.net/ghsau/article/details/16843543
- 集合源码学习(七):HashMap(Java8)
- Java8集合源码解析-HashMap
- 集合源码学习(十):HashTable(Java8)与HashMap比较
- 【Java8源码分析】集合框架-HashMap
- 集合学习--HashMap 源码初探
- Java8 - HashMap源码
- Java8 HashMap源码分析
- Java8 HashMap源码解析
- Java8 HashMap源码分析
- Java8源码-HashMap
- java8-HashMap源码分析
- Java8 HashMap源码解析
- java8 HashMap学习
- java8 HashMap学习
- Java8源码阅读之HashMap
- java8 hashMap介绍 源码分析
- Java源码集合类HashMap学习1
- Java源码集合类HashMap学习2
- 前端国际化解决方案“填坑日记”
- 细说Web API中的Blob
- Thinking in Java 读书笔记 第六章 访问权限控制
- 第七周 项目2
- 安装完CentOS7后源的配置
- 集合源码学习(七):HashMap(Java8)
- c++ 两个类互相引用的问题
- 银行定期存款
- 软考之路--操作系统
- 关于Oracle中各个命中率的计算以及相关的调优总结
- JVM设计原理与实现——虚拟机概述
- linux 或者 mac 安装mysql 忘记密码解决办法
- 1、画一个三角形
- 包含(后代)选择器和子选择器的区别