最近研究了一下java中比较常见的map类型,主要有HashMap,HashTable,LinkedHashMap和concurrentHashMap。这几种map有各自的特性和适用场景。使用方法的话,就不说了,本文重点介绍其原理和底层的实现。文章中的代码来源于jdk1.9版本。

HashMap特点及原理分析

特点

HashMap是java中使用最为频繁的map类型,其读写效率较高,但是因为其是非同步的,即读写等操作都是没有锁保护的,所以在多线程场景下是不安全的,容易出现数据不一致的问题。在单线程场景下非常推荐使用。

原理

HashMap的整体结构,如下图所示:

根据图片可以很直观的看到,HashMap的实现并不难,是由数组和链表两种数据结构组合而成的,其节点类型均为名为Entry的class(后边会对Entry做讲解)。采用这种数据结果,即是综合了两种数据结果的优点,既能便于读取数据,也能方便的进行数据的增删。

每一个哈希表,在进行初始化的时候,都会设置一个容量值(capacity)和加载因子(loadFactor)。容量值指的并不是表的真实长度,而是用户预估的一个值,真实的表长度,是不小于capacity的2的整数次幂。加载因子是为了计算哈希表的扩容门限,如果哈希表保存的节点数量达到了扩容门限,哈希表就会进行扩容的操作,扩容的数量为原表数量的2倍。默认情况下,capacity的值为16,loadFactor的值为0.75(综合考虑效率与空间后的折衷)。

  • 数据写入。以HashMap(String, String)为例,即对于每一个节点,其key值类型为String,value值类型也为String。在向哈希表中插入数据的时候,首先会计算出key值的hashCode,即key.hashCode()。关于hashCode方法的实现,有兴趣的朋友可以看一下jdk的源码(之前看到信息说有一次面试中问到了这个知识点)。该方法会返回一个32位的int类型的值,以int h = key.hashCode()为例。获取到h的值之后,会计算该key对应的哈希表中的数组的位置,计算方法就是取模运算,h%table.length。因为table的长度为2的整数次幂,所以可以用h与table.length-1直接进行位与运算,即是,index = h & (table.length-1)。得到的index就是放置新数据的位置。

    如果插入多条数据,则有可能最后计算出来的index是相同的,比如1和17,计算的index均为1。这时候出现了hash冲突。HashMap解决哈希冲突的方式,就是使用链表。每个链表,保存的是index相同的数据。

  • 数据读取。从哈希表中读取数据时候,先定位到对应的index,然后遍历对应位置的链表,找到key值和hashCode相同的节点,获取对应的value值。
  • 数据删除。 在hashMap中,数据删除的成本很低,直接定位到对应的index,然后遍历该链表,删除对应的节点。哈希表中数据的分布越均匀,则删除数据的效率越高(考虑到极端场景,数据均保存到了数组中,不存在链表,则复杂度为O(1))。

JDK源码分析

构造方法

    /**
* Constructs an empty {@code HashMap} with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
}

从构造方法中可以看到

  • 参数中的initialCapacity并不是哈希表的真实大小。真实的表大小,是不小于initialCapacity的2的整数次幂。
  • 哈希表的大小是存在上限的,就是2的30次幂。当哈希表的大小到达该数值时候,之后就不再进行扩容,只是向链表中插入数据了。

    PUT 方法

   /**
* 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 {@code key}, or
* {@code null} if there was no mapping for {@code key}.
* (A {@code null} return can also indicate that the map
* previously associated {@code null} with {@code key}.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} /**
* 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)
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))))
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;
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;
}

可以看到:

  • 给哈希表分配空间的动作,是向表中添加第一个元素触发的,并不是在哈希表初始化的时候进行的。
  • 如果对应index的数组值为null,即插入该index位置的第一个元素,则直接设置tab[i]的值即可。
  • 查看数组中index位置的node是否具有相同的key和hash如果有,则修改对应值即可。
  • 遍历数组中index位置的链表,如果找到了具有相同key和hash的node,跳出循环,进行value更新操作。否则遍历到链表的结尾,并在链表最后添加一个节点,将对应数据添加进去。
  • 方法中涉及到了TreeNode,可以暂时先不关注。

    GET 方法

 /**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
} /**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
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) {
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;
}

代码分析:

  • 先定位到数组中index位置,检查第一个节点是否满足要求 
  • 遍历对应该位置的链表,找到满足要求节点进行return

扩容操作

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

代码分析:

  • 如果就容量大于0,容量到达最大值,则不扩容。容量未到达最大值,则新容量和新门限翻倍。
  • 如果旧门限和旧容量均为0,则相当于初始化,设置对应的容量和门限,分配空间。
  • 旧数据的整理部分,非常非常的巧妙,先膜拜一下众位大神。在外层遍历node数组,对于每一个table[j],判断该node扩容之后,是属于低位部分(原数组),还是高位部分(扩容部分数组)。判断的方式就是位与旧数组的长度,如果为0则代表的是地位数组,因为index的值小于旧数组长度,位与的结果就是0;相反,如果不为零,则为高位部分数组。低位数组,添加到以loHead为头的链表中,高位数组添加到以hiHead为头的数组中。链表遍历结束,分别设置新哈希表的index位置和(index+旧表长度)位置的值。非常的巧妙。

    注意点

  • HashMap的操作中未进行锁保护,所以多线程场景下存取数据,很存在数据不一致的问题,不推荐使用
  • HashMap中key和value可以为null
  • 计算index的运算,h & (length - 1),感觉很巧妙,学习了
  • 哈希表的扩容中的数据整理逻辑,写的非常非常巧妙,大开眼界

作者:道可
链接:https://www.imooc.com/article/details/id/22943
来源:慕课网
本文原创发布于慕课网 ,转载请注明出处,谢谢合作

HashMap 阅读的更多相关文章

  1. 【JDK1.8】JDK1.8集合源码阅读——HashMap

    一.前言 笔者之前看过一篇关于jdk1.8的HashMap源码分析,作者对里面的解读很到位,将代码里关键的地方都说了一遍,值得推荐.笔者也会顺着他的顺序来阅读一遍,除了基础的方法外,添加了其他补充内容 ...

  2. HashMap原理阅读

    前言 还是需要从头阅读下HashMap的源码.目标在于更好的理解HashMap的用法,学习更精炼的编码规范,以及应对面试. 它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而 ...

  3. JDK 1.8源码阅读 HashMap

    一,前言 HashMap实现了Map的接口,而Map的类型是成对出现的.每个元素由键与值两部分组成,通过键可以找对所对应的值.Map中的集合不能包含重复的键,值可以重复:每个键只能对应一个值. 存储数 ...

  4. Java Jdk1.8 HashMap源代码阅读笔记二

    三.源代码阅读 3.元素包括containsKey(Object key) /** * Returns <tt>true</tt> if this map contains a ...

  5. JAVA8 HashMap 源码阅读

    序 阅读java源码可能是每一个java程序员的必修课,只有知其所以然,才能更好的使用java,写出更优美的程序,阅读java源码也为我们后面阅读java框架的源码打下了基础.阅读源代码其实就像再看一 ...

  6. 源码阅读之HashMap(JDK8)

    概述 HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的. HashMap最多只允许一条记录的键为null,允许多条记录 ...

  7. 【JDK8】HashMap集合 源码阅读

    JDK8的HashMap数据结构上复杂了很多,因此读取效率得以大大提升,关于源码中红黑树的增删改查,博主没有细读,会在下一篇博文中使用Java实现红黑树的增删改查. 下面是类的结构图: 代码(摘抄自J ...

  8. Java集合源码阅读之HashMap

    基于jdk1.8的HashMap源码分析. 引用于:http://blog.stormma.me/2017/05/31/Java%E9%9B%86%E5%90%88%E6%BA%90%E7%A0%81 ...

  9. HashMap与HashTable的哈希算法——JDK1.9源码阅读总结

    下面是HashTable源码中的put方法: 注意上面注释标注的地方: HashTable对于元素在哈希表中的坐标算法是: 将对象自身的哈希值key.hashCode()变为正数:hash & ...

随机推荐

  1. STM32F103片外运行代码分析

    STM32F103片外运行代码分析 STM32F103有三种启动方式: 1.从片内Flash启动: 2.从片内RAM启动: 3.从片内系统存储器启动,内嵌的自举程序,用于串口IAP. 无法直接在片外N ...

  2. win10系统下多python版本部署

    说明:win10,已安装有python3.5.2,为使用新浪云应用(SAE)支持微信公众号后台开发(SAE的python运行环境使用的是2.7.9),需部署python2.7的版本以便本地编辑调试. ...

  3. Vertical-Align你应该知道的一切

    好,我们聊聊vertical-align.这个属性主要目的用于将相邻的文本与元素对齐.而实际上,verticle-algin可以在不同上下文中灵活地对齐元素,以及进行细粒度的控制,不必知道元素的大小. ...

  4. Unity 游戏框架搭建 (十七) 静态扩展GameObject实现链式编程

    本篇本来是作为原来 优雅的QChain的第一篇的内容,但是QChain流产了,所以收录到了游戏框架搭建系列.本篇介绍如何实现GameObject的链式编程. 链式编程的实现技术之一是C#的静态扩展.静 ...

  5. 关于Hibernate的部分知识总结

    [部分内容参考地址:https://www.cnblogs.com/woniu2219/p/7111857.html] Hibernate Hibernate是一个开放源代码的对象关系映射框架,它对J ...

  6. 多用户OFDM系统资源分配研究

    首先,OFDMA 是什么? OFDM 技术的基本原理是将无线信道划分为若干互相正交的子信道,把高速串行数据流转化为低速并行子数据流,低速并行子数据流在子信道上独立传输. OFDMA 是LTE的下行多址 ...

  7. Redis(六):Redis的事务

    Redis的事务目录导航: 是什么 能干嘛 怎么玩 3阶段 3特性 是什么 可以一次执行多个命令,本质是一组命令的集合.一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞 ...

  8. 集合之TreeMap

    TreeMap 底层数据结构是二叉树 如何保证键的唯一: 利用存的特点 如何保证键的可排序: 利用取的特点 左跟右 在map中数据结构只对键有效TreeMap 有Map的键值对的特性:还可以进行排序, ...

  9. telent connection refused

    1.问题场景 Centos7 做flume案例时,telnet hadoop-senior03.itguigu.com 44444 总是Connection redused, Trying 192.1 ...

  10. linux线程篇 (三) 线程的同步

    1 互斥量 pthreat_mutex_t mymutex; //1. 创建 初始化 int pthread_mutex_init(pthread_mutex_t *mutex, const pthr ...