HashMap源码理解
2016-06-22 07:14
781 查看
导语
HashMap是常用的数据结构,了解HashMap,对提高代码的效率有很大的帮助。HashMap在JDK1.8中对数据结构进行了优化:提高了查询和删除的效率。当然,这也导致了结构更加的复杂;但通过认真阅读源码,还是可以掌握其要领的。读完本篇文章,你应该理解的内容
点击这里查看大图说明:HashMap的数据结构是个Hash表(可以理解为数组),每个槽中存放着一些节点。
一般情况下,一个槽中存放一个节点;
数据量较大时,一个槽中可能存放多个节点,此时,各个节点以链表的方式连接在一起;
当一个槽中的节点数很多时(8个以上),会以红黑树的方式来保存这些节点
源码理解
成员变量
//数组默认的大小 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //数组的最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //阈值:当槽中节点的数量逐渐增大,超过该值时,节点会从链表的形式转换成红黑树的形式 static final int TREEIFY_THRESHOLD = 8; //阈值:当槽中节点的数量逐渐减小,超过该值时,节点会从红黑树的形式转换成链表的形式 static final int UNTREEIFY_THRESHOLD = 6; //红黑树的最小容量 static final int MIN_TREEIFY_CAPACITY = 64; //数组,真正用来保存数据的容器 transient Node<K,V>[] table; //用于遍历,本篇不做介绍 transient Set<Map.Entry<K,V>> entrySet; //大小 transient int size; //修改的次数 transient int modCount; //阈值:当数组中的数据的个数大于该值时,数组会扩充 int threshold; //加载因子 final float loadFactor;
说明:从table中可以看出,HashMap最基本的数据结构是个数组;其余的成员变量单独分析是得不到什么结果的,需要结合下面的内容来理解。从常用到的put(),get(),remove()开始理解。
构造方法
在此之前,当然要看看它的构造方法是怎样的:public HashMap() { //加载因子为默认值 this.loadFactor = DEFAULT_LOAD_FACTOR; //这里并没有初始化数组 } //自定义initialCapacity,加载因子使用默认值 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //自定义initialCapacity,和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; //设置阈值;阈值大小为2的次方 //例如:initialCapacity = 17 ,阈值为 32 // initialCapacity = 5 ,阈值为 8 // initialCapacity = 55 ,阈值为 64 this.threshold = tableSizeFor(initialCapacity); } public HashMap(Map<? extends K, ? extends V> m) { //加载因子为默认值 this.loadFactor = DEFAULT_LOAD_FACTOR; //将m中的数据存到当前的Map中 putMapEntries(m, false); }
说明:前三个构造方法中,只是初始化了一些参数,没有过多的操作;第四个构造方法比较复杂,本篇读完后,再去看源码就容易理解了,这里不做讨论。
put()方法
//间接键值对 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) { // tab --数组,用来保存数据的容器 // p --i所对应数组槽中的第一个节点 // n --数组的大小 // i --当前键值对应该存储在数组中的位置 Node<K,V>[] tab; Node<K,V> p; 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 { // e-- 用来标记符合条件的节点 // k-- 键 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) //会尝试从“树”上找到相应的节点并更新 //如果没有找到,创建新的节点,添加到“树”上 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); //如果链表的长度大于阈值(8),那么将链表转换成红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //说明当前e符合条件,结束遍历 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //当有节点符合要求,更新节点中数据 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; //LinkedHashMap中会用到,这里没处理 afterNodeAccess(e); return oldValue; } } ++modCount; //如果当前的大小大于阈值,扩充数组的大小 if (++size > threshold) resize(); //LinkedHashMap中会用到,这里没处理 afterNodeInsertion(evict); return null; }
说明:实现的细节非常繁琐,但是总结起来就很简单了:
没有相应的节点,就创建节点,并放到合适的位置
有相应的节点找到对应的节点,更新其中的数据
额外说明:
put()不会重复保存key,HashSet就是利用了这点来实现去重的
LinkedHashMap会重写其中的一些方法来实现相应的特性
get()方法
//根据key找到value public V get(Object key) { Node<K,V> e; //找到相应的节点,返回value return (e = getNode(hash(key), key)) == null ? null : e.value; } //找到对应的节点 final Node<K,V> getNode(int hash, Object key) { // tab -- 数组 // first -- 数组对应槽中的第一个节点 // e -- 对应的节点 // n -- 数组的长度 // k -- 键 Node<K,V>[] tab; 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 && // always check first node ((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; }
说明:get()可以分为这么几个步骤:
锁定槽
从槽中查找相应的节点
返回合适的数据
remove()方法
//移除相应的节点 public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } //移除相应的节点 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { // tab -- 数组,用来保存数据的容器 // p -- index所对应数组槽中的第一个节点 // n -- 数组的大小 //index -- 当前键应该存储在数组中的位置 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 -- 符合要求的节点 // e -- 标记当前节点的下一个节点 // k -- key // v -- value 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; //LinkedHashMap中会用到,这里没处理 afterNodeRemoval(node); return node; } } return null; }
说明:简单来说就是:找到相应的节点并删除并且按照规则移动槽中剩余的节点。
结语
这时再去看第四个构造方法,无非就是变量传进来map,将数据封装到HashMap中来。本文对链表以及红黑树的的操作没有做进一步的分析。个人认为,阅读源码,如果过分的关注细节可能会难以把握整体的思路;当然,有些时候看源码需要关注细节,这之间需要我们进行平衡,源码看多了,这种平衡感就会有的。(链表和红黑树的操作之后的文章会单独做一些说明)
最后,再一次将核心部分,也就是最开始的那张图贴一下。
点击这里查看大图
转载请标明出处http://blog.csdn.net/qq_26411333/article/details/51723828
相关文章推荐
- 从源码安装Mysql/Percona 5.5
- c语言实现hashmap(转载)
- JDK动态代理VS CgLib
- Ubuntu 安装 JDK 问题
- 浅析Ruby的源代码布局及其编程风格
- asp.net 抓取网页源码三种实现方法
- JS小游戏之仙剑翻牌源码详解
- JS小游戏之宇宙战机源码详解
- jQuery源码分析之jQuery中的循环技巧详解
- JS hashMap实例详解
- 本人自用的global.js库源码分享
- java中原码、反码与补码的问题分析
- ASP.NET使用HttpWebRequest读取远程网页源代码
- 解析WeakHashMap与HashMap的区别详解
- jdk与jre的区别 很形象,很清晰,通俗易懂
- jdk中String类设计成final的原由
- win7下安装 JDK 基本流程
- jdk环境变量配置
- win2003 jsp运行环境架设心得(jdk+tomcat)
- windows linux jdk安装配置方法