数据结构

在JDK1.8之前,HashMap是基于链式哈希实现的,而在JDK1.8之后,为了提高冲突节点的访问性能,在链式哈希实现的基础上,在哈希表大小超过64时,针对冲突节点链条,如果节点数量超过8个,则升级为红黑树,小于等于6个时,则降级为链表结构。

链式哈希

  • 链式哈希是一个数组结构,数组元素为链表或者红黑树。如下为HashMap的内部数据存储结构,也是链式哈希的实现。其中Node为一个key的hash值相同的数据的链表(或红黑树)的头节点,即为冲突节点的链表。

    /*** The table, initialized on first use, and resized as* necessary. When allocated, length is always a power of two.* (We also tolerate length zero in some operations to allow* bootstrapping mechanics that are currently not needed.)*/
    transient Node<K,V>[] table;
    

链表节点

  • 如下为链表节点的数据结构设计:包括key,value,key的hash值,当前节点在链表中的下一个节点next。

  • 该链表内的所有节点的key的hash值是相同的,链表头结点存放在哈希表table中,基于hash值与table大小获取该链表头结点在table中的位置下标。

    /*** Basic hash bin node, used for most entries.  (See below for* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)*/
    static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> 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;}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;}
    }
    

红黑树节点

  • 与链表节点一样,红黑树中存放key的hash值相同的节点集合,其中红黑树根节点root存放在哈希表table中,对应的table数组下标也是基于key的hash值和数组table大小获取。

    /*** Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn* extends Node) so can be used as extension of either regular or* linked node.*/
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}/*** Returns root of tree containing this node.*/final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}}...}
    

核心设计

table数组容量与阈值

/*** The default initial capacity - MUST be a power of two.*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/*** The maximum capacity, used if a higher value is implicitly specified* by either of the constructors with arguments.* MUST be a power of two <= 1<<30.*/
static final int MAXIMUM_CAPACITY = 1 << 30;/*** The load factor used when none specified in constructor.*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • DEFAULT_INITIAL_CAPACITY: table数组默认大小为16,即在应用代码中,创建一个HashMap时,不指定容量,则之后在第一次添加数据到该HashMap,首先分配一个大小为16的数组table。

  • MAXIMUM_CAPACITY:数组最大大小,超过则不再进行拓容,大小为2的30次方。

  • DEFAULT_LOAD_FACTOR:数组拓容阈值,即在当前数组存放的数据节点达到容量的0.75时,则进行数组拓容,具体拓容逻辑在resize方法定义。

构建红黑树阈值

  • 默认哈希表对应的数组的每个元素是一个链表,具体为存放链表头节点,而在冲突节点较多时,即存在较多key的hash值相同的元素,则会导致链表过长,在应用代码中获取某个key的值value,时间复杂度就会增加,即默认的O(1)变为了O(N),其中N为该链表长度。

  • 所以为了解决这种情况下的性能问题,JDK1.8提供了从链表转为红黑树,或者从红黑树还原为链表的设计,在红黑树中获取某个节点的时间复杂度为O(logN),具体阈值如下:

    /*** The bin count threshold for using a tree rather than list for a* bin.  Bins are converted to trees when adding an element to a* bin with at least this many nodes. The value must be greater* than 2 and should be at least 8 to mesh with assumptions in* tree removal about conversion back to plain bins upon* shrinkage.*/
    static final int TREEIFY_THRESHOLD = 8;/*** The bin count threshold for untreeifying a (split) bin during a* resize operation. Should be less than TREEIFY_THRESHOLD, and at* most 6 to mesh with shrinkage detection under removal.*/
    static final int UNTREEIFY_THRESHOLD = 6;/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*/
    static final int MIN_TREEIFY_CAPACITY = 64;
    
  • TREEIFY_THRESHOLD:构造红黑树的阈值,默认为8,即链表的元素个数超过8个的时候,则将链表转为红黑树,不过前提是数组大小大于MIN_TREEIFY_CAPACITY。使用8作为阀值的原因是:红黑树的操作复杂度为O(logN),如果是8则平均查找次数为3,如果使用链表,则平均查找次数为8/2=4,故红黑树性能更高;

  • MIN_TREEIFY_CAPACITY:触发红黑树化的前提条件,默认值为64,即哈希表数组table的大小超过64的时候,如果某个数组元素对应的链表的长度超过TREEIFY_THRESHOLD,则将该数组元素(即链表头结点)对应的链表转为红黑树结构。

  • UNTREEIFY_THRESHOLD:从红黑树退化为链表的条件,默认为6,即当红黑树中节点个数不超过6时,则退化为链表。使用6作为退化阀值原因是:如果使用链表,平均查找次数为6/2=3,速度也很快,如果使用树,则转为树结构和调整树也需要开销,故可以直接使用链表。中间使用7作为差值,主要是有效防止树和链表的频繁转换。

核心操作设计

数组大小capacity调整

  • HashMap内部存储数据所用的链式哈希表是基于数组实现的,即数组 + 链表或者数组 + 红黑树,所以在往HashMap存放数据之前,需要指定数组大小来创建这个数组。

  • 在存放某个数据时,需要根据该数据的key的hash值与数组大小取模,从而确定该数据所在的链表(具体为链表头结点)在数组中的位置下标。在HashMap的设计中,为了提高性能,采用的是位运算的方式,而不是使用%的方式取模,具体方式如下:

    (capacity - 1) & hash
    
  • 这种方式的实现基础为capacity的值为2的N次方,则capacity - 1的值对应二进制就全是1了,如16的二进制为:10000,15的二进制为:01111,则01111与任何数字的与运算的结果为0到15,如:

    01111 & 00010 = 00010,即15与2取模等于2
    01111 & 100001 = 00001,即15与33取模等于1
    
  • 所以在指定HashMap的容量capacity时,则内部会将capacity调整为2的N次方,即调整后的capacity大于或等于应用代码指定的capacity。调整实现为:在tableSizeFor方法定义,如下:

    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);
    }/*** Returns a power of two size for the given target 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;
    }
    

put设值

  • put操作往HashMap存放数据,在内部主要是在putVal方法定义实现逻辑。具体过程请看代码注释:

    /*** 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) {// tab:指向哈希表table// p:遍历哈希表table时,存放遍历到的数组元素Node<K,V>[] tab; Node<K,V> p; int n, i;// table为null,还没初始化或者大小为0,// 则调用resize先创建数组或者对数组进行拓容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;// 1.如果数组元素(链表头结点)就是该节点,// 即此次put操作是更新,则更新值value即可;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 2. 或者之前已经将链表升级为了红黑树,// 则往该红黑树插入该节点;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 3. 或者链表头结点不是该元素,// 则遍历链表在链表尾部插入该节点,// 在链表尾部插入之后,// 需要检查一下是否需要将该链表升级为红黑树else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 检查链表长度是否超过了8if (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;}}// 更新节点值valueif (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);// 直接返回更新之前的值// 更新操作不是结构性修改,故不需要往下继续执行。return oldValue;}}// 记录结构性修改(增加或删除节点)次数// 用于实现迭代器iterator的fail-fast快速失败++modCount;// 如果插入后,哈希表大小超过了阈值,// 则resize拓容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
    }
    

resize扩容

  • resize操作主要是对哈希表数组table进行拓容或初始化,拓容为每次增大为原来的2倍,从而保证容量capacity为2的N次方的设计。

  • 拓容阈值threshold:threshold = capacity * load factor,即默认为容量capacity的0.75,当存放的元素达到容量的3/4时,进行拓容resize操作。

    /*** 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;// 原来的容量大于0,即哈希表数组table已经存在了的if (oldCap > 0) {// 当前容量已经是最大容量,则直接返回,不再进行拓容。if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 将容量和拓容阈值都拓展为原来的2倍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// 旧容量小于0,则容量设值为拓展阈值newCap = oldThr;// 初始化哈希数组,使用默认值else {               // zero initial threshold signifies using defaults// 容量等于初始大小16newCap = DEFAULT_INITIAL_CAPACITY;// 拓容阈值等于16*0.75=12newThr = (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"})// 创建一个新的哈希数组,容量为原来的2倍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) {// e已经存放了原来的数组元素,// 则将旧数组的元素置为null,便于GC回收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);// 将链表(具体为链表头结点)// 复制到新数组中// 由于数组拓容后,大小capacity变大了,// 故单个链表中的元素的hash与capacity取模会发生变化,// 故旧数组的某个数组元素,// 对应的链表也要拆分成两条链表,放在新数组的两个位置中。// 同时需要保持链表中元素的位置,// 即链表前半部分对应的新的子链表对应的新数组中的位置为// 该链表在旧数组的位置下标// 链表后半部分对应的新的子链表放在// 新数组后面的一个位置下标else { // preserve order// lo为链表的前半部分// hi为链表的后半部分Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;// e.hash与oldCap,而不是oldCap-1,而oldCap为2的N次方,二进制为,// 如16的二进制为10000// 则 e.hash & oldCap == 0,表示e所在链表,// 在拓展后在新数组中的位置,// 等于原来在旧数组中的,// 所以放到lo部分。if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 拓容后,hash值与新的capacity取模不等于与旧的capacity的,// 故放到hi部分。else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;// lo链表放到新数组下标j中newTab[j] = loHead;}if (hiTail != null) {// hi链表放到新数组下标j+oldCap中hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
    }
    

treeifyBin构造红黑树

  • 主要用来将某个数组元素对应的链表,转为红黑树结构,从而解决链表长度过长时的访问性能问题。

    /*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.*/
    final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 当哈希表为null即还没初始化,// 或者哈希表对应的数组大小小于64时,// 则resize,先不将该链表转为红黑树,// 即使该链表长度超过了8,// 主要是因为当前的哈希表还是比较小的。if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();// 从哈希表中取出该元素,将该元素(链表头结点)// 对应的链表调整为红黑树else if ((e = tab[index = (n - 1) & hash]) != null) {TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}
    }
    

clear清空

  • 清空HashMap,具体为将哈希表数组table的每个元素置为null,则该元素对应的链表或红黑树则没有被引用了,可以被GC回收。

    /*** Removes all of the mappings from this map.* The map will be empty after this call returns.*/
    public void clear() {Node<K,V>[] tab;modCount++;if ((tab = table) != null && size > 0) {size = 0;for (int i = 0; i < tab.length; ++i)tab[i] = null;}
    }
    

哈希表节点Node集合

EntrySet:哈希表所有节点集合

  • 从数组的每个元素对应的链表中取出链表节点放到该集合

keySet:哈希表key集合

  • 哈希表所有节点的key集合

Values:哈希表value集合

  • 哈希表所有节点的value集合

链表迭代器基类:HashIterator

  • 主要被以上集合用来遍历哈希表数组,然后遍历每个数组对应的链表,从而获取所有的数据,即Node节点,然后放到集合中。

  • 派生类包括:KeyIterator,ValueIterator,EntryIterator,其中下一个节点都依赖nextNode方法,从链表头结点开始,获取该链表的下一个节点。

  • 迭代器是快速失败fail-fast的,即在使用过程中,如果往HashMap中添加或删除了数据,则抛异常退出。

    abstract class HashIterator {Node<K,V> next;        // next entry to returnNode<K,V> current;     // current entryint expectedModCount;  // for fast-failint index;             // current slotHashIterator() {expectedModCount = modCount;Node<K,V>[] t = table;// 初始化为nullcurrent = next = null;// 从数组table第一个数组元素开始index = 0;if (t != null && size > 0) { // advance to first entry// 遍历哈希表数组table,// 获取第一个数组节点(即某个链表头节点)// 不为null的节点,// 即整个哈希表从数组和链表两个角度,// 第一个节点Node作为迭代的开始节点。do {} while (index < t.length && (next = t[index++]) == null);}}public final boolean hasNext() {return next != null;}final Node<K,V> nextNode() {Node<K,V>[] t;Node<K,V> e = next;// 并发进行了结构性修改if (modCount != expectedModCount)throw new ConcurrentModificationException();if (e == null)throw new NoSuchElementException();// 先遍历数组的元素对应的链表// 遍历完该链表后,// 遍历数组下一个元素对应的链表if ((next = (current = e).next) == null && (t = table) != null) {do {} while (index < t.length && (next = t[index++]) == null);}return e;}public final void remove() {Node<K,V> p = current;if (p == null)throw new IllegalStateException();// 并发进行了结构性修改if (modCount != expectedModCount)throw new ConcurrentModificationException();current = null;K key = p.key;removeNode(hash(key), key, null, false, false);expectedModCount = modCount;}
    }
    

线程安全

  • HashMap不是线程安全的,故如果需要保证多线程访问的并发安全性,需要使用ConcurrentHashMap,或者使用Collections.synchronizedMap方法来对HashMap进行包装成类型为SynchronizedMap的,线程安全的map。
  • ConcurrentHashMap主要是基于CAS来实现乐观锁来实现线程安全;
  • SynchronizedMap的内部实现,主要是通过synchronized关键字结合一个对象锁mutex,来对会产生并发问题的操作,如get,put,remove,clear,contains操作进行同步操作。

JDK1.8源码分析:HashMap相关推荐

  1. 【集合框架】JDK1.8源码分析之HashMap(一)

    转载自  [集合框架]JDK1.8源码分析之HashMap(一) 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大 ...

  2. 【集合框架】JDK1.8源码分析HashSet LinkedHashSet(八)

    一.前言 分析完了List的两个主要类之后,我们来分析Set接口下的类,HashSet和LinkedHashSet,其实,在分析完HashMap与LinkedHashMap之后,再来分析HashSet ...

  3. 【JUC】JDK1.8源码分析之ArrayBlockingQueue(三)

    一.前言 在完成Map下的并发集合后,现在来分析ArrayBlockingQueue,ArrayBlockingQueue可以用作一个阻塞型队列,支持多任务并发操作,有了之前看源码的积累,再看Arra ...

  4. JDK1.8源码分析:可重入锁ReentrantLock和Condition的实现原理

    synchronized的用法和实现原理 synchronized实现线程同步的用法和实现原理 不足 synchronized在线程同步的使用方面,优点是使用简单,可以自动加锁和解锁,但是也存在一些不 ...

  5. JDK1.8源码分析之HashMap(一) (转)

    一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...

  6. 基于JDK1.8---HashMap源码分析

    基于JDK1.8-HashMap源码简要分析 HashMap继承关系 HashMap:根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不 ...

  7. 【JUC】JDK1.8源码分析之ConcurrentHashMap

    一.前言 最近几天忙着做点别的东西,今天终于有时间分析源码了,看源码感觉很爽,并且发现ConcurrentHashMap在JDK1.8版本与之前的版本在并发控制上存在很大的差别,很有必要进行认真的分析 ...

  8. Java中ConcurrentHashMap底层实现原理(JDK1.8)源码分析2

    https://blog.csdn.net/programmer_at/article/details/79715177 https://blog.csdn.net/qq_41737716/categ ...

  9. 【集合框架】JDK1.8源码分析之IdentityHashMap(四)

    一.前言 前面已经分析了HashMap与LinkedHashMap,现在我们来分析不太常用的IdentityHashMap,从它的名字上也可以看出来用于表示唯一的HashMap,仔细分析了其源码,发现 ...

  10. jdk1.8 源码分析导图

    以下总结全部基于 jdk1.8,详细源码分析见 GitHub 链接:https://github.com/zchen96/jdk1.8-source-code-read 一.非并发 幕布导图链接:ht ...

最新文章

  1. java mqtt 断开连接,可以将MQTT Paho客户端断开连接吗?
  2. 在虚拟机中安装LINUX
  3. jquery的鼠标移入移出事件hover、mouseenter、mouseleave、mouseover、mouseout
  4. 密码学专题 非对称加密算法指令概述 RSA
  5. 趣挨踢 | 跳槽季,让我们一起攻克算法吧!
  6. html网页中使用mock,关于Mock.js使用
  7. mysql查处所有表中的新数据,mysql – 对所有表执行查询并在新表中填入数据
  8. solidity教程(四)僵尸作战系统
  9. serv-u and hway3.0
  10. 【React】项目中组件化使用svg格式的图片
  11. GNS3快捷安装指南
  12. 多线程与高并发day04
  13. 排序法学习之插入排序(python3实现)
  14. 信息量理解、信息熵公式的推导
  15. 未来十年Python的前景会怎样?(知乎)
  16. 玩转代码|免费获取在线Logo设计网站的Logo
  17. Cornerstone忽略不必要的文件
  18. 深大uooc大学生心理健康章节答案第八章
  19. 红米手机4X完美卡刷开发版开启Root超级权限的流程
  20. 【教程】Git在Eclipse中的安装和基本使用

热门文章

  1. 深度学习框架TensorFlow取名由来
  2. 前端开发面试经历分享(苏宁总部-苏宁易购面试题)
  3. IEC104测试工具软件更新
  4. 好用的word转换成pdf转换器!你值得拥有
  5. 如何入手C语言,怎样去学C语言
  6. Volatility取证工具使用学习日记
  7. 基于百度AI开发平台实现人脸识别登陆-Springboot
  8. 哈希码相同,值一定相同?
  9. 题目98-4:n个人(n<=100)围成一圈,从第一个人开始报数,数到m的人出列,再由下一个人重新从1开始报数,数到m的人再出圈,……,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
  10. 软件定制服务收费流程和标准