Java集合学习之HashMap 一(JDK1.8)
来源:互联网 发布:linux swap分区 编辑:程序博客网 时间:2024/05/17 04:53
Java集合学习之HashMap 一(JDK1.8)
- 既然走上了研发这条路, 首选的语言又是Java, 吃饭的家伙就需要了解的透彻一些
- 要想深入使用Java, 集合一定是绕不过的坎儿, 从初级到高级甚至资深的水平, 反正是都要过一遍JDK中集合的源代码
- 下载JDK源码的时候已经是
Java1.8
了, 所以和网上的博客们和教程们可能会有一点冲突
HashMap概述
- 是
Map接口
基于散列表的实现, 继承自抽象类AbstractMap
- 插入和查询Key-Value对的开销是固定的
- 可以通过构造器设置
容量
和负载因子
, 以调整容器的性能 - HashMap不保证映射的顺序, 特别是它不保证该顺序恒久不变
- HashMap**非线程安全**, 如果多个线程同时访问一个 HashMap, 如果其中有一个线程需要从结构上(指添加或者删除一个或多个映射关系的任何操作)做了修改, 则需要通过外部来保持同步
HashMap的数据结构
HashMap的底层数据结构是通过数组和链表的结合来实现的, 如图所示:
通常的文章都把数组的每个元素称作一个桶
从HashMap的构造函数来看:
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);}
和之前版本的JDK不同, 在初始化HashMap的时候并不会创建存放键值对
的数组,该数组的创建在第一次使用的时候在resize()
方法中执行
HashMap维护几个重要的数据域:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的容量static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量, 2的30次方static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认的负载因子static final int TREEIFY_THRESHOLD = 8; // 每个桶的阈值, 即每个桶装多少为上?static final int UNTREEIFY_THRESHOLD = 6;transient Node<K,V>[] table;
JDK1.8
存储键值对
的对象不再是Entry<K,V>
, 而是改用Node<K,V>
,不过基本实现没有变化:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ……}
暂时猜测与新增的TreeNode有关
Node是一个静态类,是存放数据元素的对象,除了包含key和value的值外,还包含了一个指向另一个Node的next指针,从而构建了拥有相同hash值的键值对
的链表
HashMap的核心方法解读
存储键值对
暴露出来的方法接口依然是put()
,但是真正的实现是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; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 初次使用该HashMap的对象时, 给table分配空间 if ((p = tab[i = (n - 1) & hash]) == null) // key值经过hash后得到其在table中桶的下标索引 /* table中在该下标索引的桶中没有元素, 则直接把该Node元素放置到该桶中 */ 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)))) e = p; // 桶中的第一个元素的key值和要存储的元素的key值相同 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) { /* 遍历到链表的尾部, 没有找到相同的key, 则新增一个Node */ p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /* 桶中的某个元素的key值和要存储的元素的key值相同 */ if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /* 此时的e是某个Node<K, V>,可能是原先就存在桶中的, 也可能是新增的 */ if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // 更新value afterNodeAccess(e); return oldValue; // 返回"旧"的value } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null;}
总结一下put
操作的步骤:
- 第一次执行put操作时,会给HashMap底层的数据结构table分配空间
- 对key值进行hash后获得桶的下标索引
- 如果在该下标索引的桶中没有存放数据, 则直接把该数据放置在这个桶中
- 如果该下标索引的桶中有数据, 则遍历该桶的链表
- 如果找到相同的key值, 则更新该Node的value值, 并返回旧的value值; 如果key值不存在, 则new 一个新的Node, 返回null
- 当这个HashMap的size大于阈值的时候, 执行resize()方法重新给table分配空间
简单讨论一下关于key值的hash函数
- 当系统决定存储HashMap中的键值对时, 完全没有考虑Node中的value, 仅仅只是根据key来计算并决定每个Node属于哪个桶
- 我们完全可以把Map集合中的value当成key的附属品, 当系统决定了key的存储位置之后, value 随之保存在那里即可
对与key的hash计算并决定其存放的桶, 共有两步:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}/* 桶的下标索引 */i = (n - 1) & hash // n = table.length
- 桶的下标索引算法实际上是一种技巧性的
取模运算
, 因为HashMap的底层数组的容量总是2的n次方 - 当length总是2的n次方时, h&(length-1)运算等价于对length取模, 也就是h%length, 但是&比%具有更高的效率
读取键值对
暴露出来的方法接口依然是get()
,但是真正的实现是getNode()
方法:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) { 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) { // 判断是否能通过key值的hash找到相应的链表 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) // 根据key值确定链表的第一个元素就是满足的元素 return first; /* 遍历该链, 寻找是否有该key值的value */ 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
操作的步骤:
- 根据key值的hash值判断相应下标索引的桶是否存放数据元素, 没有则返回null
- 根据key值判断该桶的第一个数据元素是不是就是要找的数据, 找到则返回该Node
- 在上一步没有找到想要的值, 则遍历该桶的链表, 根据key值来判断是否存在正在寻找的数据元素, 找到则返回该Node
- 没有找到相应的key, 返回null
resize()方法
和之前版本的resize()方法相比, JDK1.8中还承担了初始化table时分配空间的任务, 初始的table的容量大小是16(DEFAULT_INITIAL_CAPCITY
), 初始的阈值是12(DEFAULT_LOAD_FACTOR
* DEFAULT_INITIAL_CAPCITY
)
resize由两个步骤组成:
- 扩容, 以成倍的方式(乘以2)
- 重新hash, 保持各个桶原有的链表的元素的顺序不变
0 0
- Java集合学习之HashMap 一(JDK1.8)
- JDK1.8源码学习之 HashMap.java
- java集合-HashMap(JDK1.8)
- java 集合学习之hashMap
- Java集合类框架学习 4.3 —— HashMap(JDK1.8)
- Java源码分析之HashMap(JDK1.8)
- Java源码分析之HashMap(JDK1.8)
- java集合(4):HashMap源码分析(jdk1.8)
- Java基础之源码阅读(一):jdk1.8的HashMap
- java学习之集合---浅析HashMap
- Java Jdk1.8 HashMap源码阅读笔记一
- Java集合之HashMap
- java集合之HashMap
- java集合之HashMap
- java集合之HashMap
- Java集合之HashMap
- Java集合之HashMap
- Java集合之HashMap
- Hrbust 2061消息队列(优先队列应用)
- S5PV210的时钟系统(上)
- SpringMVC+Spring4+hibernate4实例
- php+中文分词scws+sphinx+mysql打造千万级数据全文搜索
- Html总结(二)
- Java集合学习之HashMap 一(JDK1.8)
- Ubuntu 14.04下安装eclipse搭建C++开发环境
- Android SharedPreference工具类
- Coreseek、Sphinx-for-chinaese、Sphinx+Scws区别
- 外部可屏蔽中断服务程序
- caffe中的normalization_layer
- 初识QT1
- tomcat: The requested resource is not available.一种可能的原因
- java 中的访问修饰符权限