JDK1.8 HashMap源码分析
Posted 牧野流冰87
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK1.8 HashMap源码分析相关的知识,希望对你有一定的参考价值。
一、HashMap概述
在JDK1.8之前,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
下图中代表jdk1.8之前的hashmap结构,左边部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。(此图借用网上的图)
图一、jdk1.8之前hashmap结构图
jdk1.8之前的hashmap都采用上图的结构,都是基于一个数组和多个单链表,hash值冲突的时候,就将对应节点以链表的形式存储。如果在一个链表中查找其中一个节点时,将会花费O(n)的查找时间,会有很大的性能损失。到了jdk1.8,当同一个hash值的节点数不小于8时,不再采用单链表形式存储,而是采用红黑树,如下图所示(此图是借用的图)
图二、jdk1.8 hashmap结构图
二、重要的field
- //table就是存储Node类的数组,就是对应上图中左边那一栏,
- /**
- * 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;
- /**
- * The number of key-value mappings contained in this map.
- * 记录hashmap中存储键-值对的数量
- */
- transient int size;
- /**
- * hashmap结构被改变的次数,fail-fast机制
- */
- transient int modCount;
- /**
- * The next size value at which to resize (capacity * load factor).
- * 扩容的门限值,当size大于这个值时,table数组进行扩容
- */
- int threshold;
- /**
- * The load factor for the hash table.
- *
- */
- float loadFactor;
- /**
- * The default initial capacity - MUST be a power of two.
- * 默认初始化数组大小为16
- */
- 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;
- /**
- * 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;
三、构造函数
- //可以自己指定初始容量和装载因子
- 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;
- //先移位再或运算,最终保证返回值是2的整数幂
- 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;
- }
- /**
- * Constructs an empty <tt>HashMap</tt> with the specified initial
- * capacity and the default load factor (0.75).
- *
- * @param initialCapacity the initial capacity.
- * @throws IllegalArgumentException if the initial capacity is negative.
- */
- //当知道所要构建的数据容量的大小时,最好直接指定大小,提高效率
- public HashMap(int initialCapacity) {
- this(initialCapacity, DEFAULT_LOAD_FACTOR);
- }
- /**
- * Constructs an empty <tt>HashMap</tt> with the default initial capacity
- * (16) and the default load factor (0.75).
- */
- public HashMap() {
- this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
- }
- //将map直接放入hashmap中
- public HashMap(Map<? extends K, ? extends V> m) {
- this.loadFactor = DEFAULT_LOAD_FACTOR;
- putMapEntries(m, false);
- }
- final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
- int s = m.size();
- if (s > 0) {
- if (table == null) { // pre-size
- float ft = ((float)s / loadFactor) + 1.0F;
- int t = ((ft < (float)MAXIMUM_CAPACITY) ?
- (int)ft : MAXIMUM_CAPACITY);
- if (t > threshold)
- threshold = tableSizeFor(t);
- }
- else if (s > threshold)
- resize();
- for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
- K key = e.getKey();
- V value = e.getValue();
- putVal(hash(key), key, value, false, evict);
- }
- }
- }
- /**
- * Basic hash bin node, used for most entries. (See below for
- * TreeNode subclass, and in LinkedMyHashMap for its Entry subclass.)
- */
- 在hashMap的结构图中,hash数组就是用Node型数组实现的,许多Node类通过next组成链表,key、value实际存储在Node内部类中。
- public 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;
- }
- }
四、重要的方法分析
1.put方法
- /**
- * Associates the specified value with the specified key in thismap.
- * If the map previously contained a mapping for the key, the old
- * value is replaced.
- *
- */
- public V put(K key, V value) {
- return putVal(hash(key), key, value, false, true);
- }
- static final int hash(Object key) {
- int h;
- //key的值为null时,hash值返回0,对应的table数组中的位置是0
- return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- }
- /**
- * 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;
- //先将table赋给tab,判断table是否为null或大小为0,若为真,就调用resize()初始化
- if ((tab = table) == null || (n = tab.length) == 0)
- n = (tab = resize()).length;
- //通过i = (n - 1) & hash得到table中的index值,若为null,则直接添加一个newNode
- if ((p = tab[i = (n - 1) & hash]) == null)
- tab[i] = newNode(hash, key, value, null);
- else {
- //执行到这里,说明发生碰撞,即tab[i]不为空,需要组成单链表或红黑树
- Node<K,V> e; K k;
- if (p.hash == hash &&
- ((k = p.key) == key || (key != null && key.equals(k))))
- //此时p指的是table[i]中存储的那个Node,如果待插入的节点中hash值和key值在p中已经存在,则将p赋给e
- e = p;
- //如果table数组中node类的hash、key的值与将要插入的Node的hash、key不吻合,就需要在这个node节点链表或者树节点中查找。
- else if (p instanceof TreeNode)
- //当p属于红黑树结构时,则按照红黑树方式插入
- e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
- else {
- //到这里说明碰撞的节点以单链表形式存储,for循环用来使单链表依次向后查找
- for (int binCount = 0; ; ++binCount) {
- //将p的下一个节点赋给e,如果为null,创建一个新节点赋给p的下一个节点
- if ((e = p.next) == null) {
- p.next = newNode(hash, key, value, null);
- //如果冲突节点达到8个,调用treeifyBin(tab, hash),这个treeifyBin首先回去判断当前hash表的长度,如果不足64的话,实际上就只进行resize,扩容table,如果已经达到64,那么才会将冲突项存储结构改为红黑树。
- if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
- treeifyBin(tab, hash);
- break;
- }
- //如果有相同的hash和key,则退出循环
- if (e.hash == hash &&
- ((k = e.key) == key || (key != null && key.equals(k))))
- break;
- p = e;//将p调整为下一个节点
- }
- }
- //若e不为null,表示已经存在与待插入节点hash、key相同的节点,hashmap后插入的key值对应的value会覆盖以前相同key值对应的value值,就是下面这块代码实现的
- if (e != null) { // existing mapping for key
- V oldValue = e.value;
- //判断是否修改已插入节点的value
- if (!onlyIfAbsent || oldValue == null)
- e.value = value;
- afterNodeAccess(e);
- return oldValue;
- }
- }
- ++modCount;//插入新节点后,hashmap的结构调整次数+1
- if (++size > threshold)
- resize();//HashMap中节点数+1,如果大于threshold,那么要进行一次扩容
- afterNodeInsertion(evict);
- return null;
- }
- 2.扩容函数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;//定义临时Node数组型变量,作为hash table
- //读取hash table的长度
- int oldCap = (oldTab == null) ? 0 : oldTab.length;
- int oldThr = threshold;//读取扩容门限
- int newCap, newThr = 0;//初始化新的table长度和门限值
- if (oldCap > 0) {
- //执行到这里,说明table已经初始化
- 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
- //用构造器初始化了门限值,将门限值直接赋给新table容量
- newCap = oldThr;
- else {
- // zero initial threshold signifies using defaults
- //老的table容量和门限值都为0,初始化新容量,新门限值,在调用hashmap()方式构造容器时,就采用这种方式初始化
- newCap = DEFAULT_INITIAL_CAPACITY;
- newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
- }
- if (newThr == 0) {
- //如果门限值为0,重新设置门限
- float ft = (float)newCap * loadFactor;
- newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
- (int)ft : Integer.MAX_VALUE);
- }
- threshold = newThr;//更新新门限值为threshold
- @SuppressWarnings({"rawtypes","unchecked"})
- //初始化新的table数组
- Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
- table = newTab;
- //当原来的table不为null时,需要将table[i]中的节点迁移
- if (oldTab != null) {
- for (int j = 0; j < oldCap; ++j) {
- Node<K,V> e;
- //取出链表中第一个节点保存,若不为null,继续下面操作
- if ((e = oldTab[j]) != null) {
- oldTab[j] = null;//主动释放
- if (e.next == null)
- //链表中只有一个节点,没有后续节点,则直接重新计算在新table中的index,并将此节点存储到新table对应的index位置处
- newTab[e.hash & (newCap - 1)] = e;
- else if (e instanceof TreeNode)
- //若e是红黑树节点,则按红黑树移动
- ((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 {
- //下面这段暂时没有太明白,通过e.hash & oldCap将链表分为两队,参考知乎上的一段解释
- /**
- * 把链表上的键值对按hash值分成lo和hi两串,lo串的新索引位置与原先相同[原先位
- * j],hi串的新索引位置为[原先位置j+oldCap];
- * 链表的键值对加入lo还是hi串取决于 判断条件if ((e.hash & oldCap) == 0),因为* capacity是2的幂,所以oldCap为10...0的二进制形式,若判断条件为真,意味着
- * oldCap为1的那位对应的hash位为0,对新索引的计算没有影响(新索引
- * =hash&(newCap-*1),newCap=oldCap<<2);若判断条件为假,则 oldCap为1的那位* 对应的hash位为1,
- * 即新索引=hash&( newCap-1 )= hash&( (oldCap<<2) - 1),相当于多了10...0,
- * 即 oldCap
- * 例子:
- * 旧容量=16,二进制10000;新容量=32,二进制100000
- * 旧索引的计算:
- * hash = xxxx xxxx xxxy xxxx
- * 旧容量-1 1111
- * &运算 xxxx
- * 新索引的计算:
- * hash = xxxx xxxx xxxy xxxx
- * 新容量-1 1 1111
- * &运算 y xxxx
- * 新索引 = 旧索引 + y0000,若判断条件为真,则y=0(lo串索引不变),否则y=1(hi串
- * 索引=旧索引+旧容量10000)
- */
- 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;
- }
- } 以上是关于JDK1.8 HashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章