JDK源码HashMap源码分析

Posted 代码狂魔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK源码HashMap源码分析相关的知识,希望对你有一定的参考价值。

Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。

总结

JDK1.7

  • 数组加链表,"拉链法"解决hash冲突

  • 底层数组长度总是为2的幂次方。这是因为在此条件下hash & (length - 1) == hash % length,而且&比%的效率更高,(hash % length总是小于length的,因此可以用来计算元素在桶中的位置)

    • 默认长度16,会动态增长为32,64,128...,就算初始化的时候指定为11,其实底层的数组长度还是16

  • 负载因子默认是0.75,是可以修改的,扩容阈值=数组长度*负载因子

    • 假设数组长度为16,则扩容阈值为16*0.75=12,当实际所放元素大于12时,则触发扩容操作

  • 自动扩容,非常消耗性能

  • 当hash严重冲突时,链表会越来越长严重影响效率,时间复杂度最长为O(N)

  • 线程不安全(需要线程安全的请使用ConcurrentHashMap),多线程会引发链表死循环

JDK1.8后的优化

  • 当链表长度超过8的时候则直接转换成红黑树,查询效率为O(logN)

  • 扩容时会均匀分配元素,而JDK1.7会原封不动的拷贝过来

  • 多线程会引发链表死循环的问题已解决

测试

//指定初始容量为11,但是底层数组的长度还是会初始化为16,具体看tableSizeFor方法
Map<Integer,String> map = new HashMap(11);


//初始化map
map = new HashMap<>();//初始化容量为16的HashMap
map.put(1,"A");//索引位置 1 % 16 = 1;放入第一个元素的时候会初始化map


//元素放在不同的桶中
map = new HashMap<>();//初始化容量为16的HashMap
map.put(1,"A");//索引位置 1 % 16 = 1;放入第一个元素的时候会初始化map
map.put(2,"B");//索引位置 2 % 16 = 2


//hash碰撞,产生链表
map = new HashMap<>();//初始化容量为16的HashMap
map.put(1,"A");//索引位置   1 % 16 = 1;放入第一个元素的时候会初始化map
map.put(17,"B");//索引位置 17 % 16 = 1,索引相同,hash碰撞,产生链表


//hash碰撞,产生红黑树
map = new HashMap<>();//初始化容量为16的HashMap
map.put(1,"A");   //索引位置   1 % 16 = 1;放入第一个元素的时候会初始化map
map.put(17,"B");  //索引位置 17 % 16 = 1,hash碰撞,链表长度2
map.put(33,"D");  //索引位置 33 % 16 = 1,hash碰撞,链表长度3
map.put(49,"E");  //索引位置 49 % 16 = 1,hash碰撞,链表长度4
map.put(65,"F");  //索引位置 65 % 16 = 1,hash碰撞,链表长度5
map.put(81,"G");  //索引位置 81 % 16 = 1,hash碰撞,链表长度6
map.put(97,"H");  //索引位置 97 % 16 = 1,hash碰撞,链表长度7
map.put(113,"I"); //索引位置 113 % 16 = 1,hash碰撞,链表长度8
map.put(129,"J"); //索引位置 129 % 16 = 1,hash碰撞,链表长度9,此时产生红黑树,调用treeifyBin(tab, hash)


JDK1.8

成员变量

/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • DEFAULT_INITIAL_CAPACITY表示初始化容量大小为2^4 = 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;
  • 最大容量为2^30

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • 负载因子默认为0.75

/**
* 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;
  • 当链表长度大于8的时候,链表将转换成红黑树

/**
* 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;
  • table是真正存放数据的数组

/**
* The number of key-value mappings contained in this map.
*/
transient int size;
  • size表示当前map实际存放元素数量的大小

/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
  • 结构化修改次数的大小

/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
int threshold;
  • 需要扩容的阈值,当size和threshold相等时会触发扩容操作

/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
  • 负载因子,默认为DEFAULT_LOAD_FACTOR,可构造map的时候传入

容量capacity(C),负载因子loadFactor(L),扩容阈值threshold(T)和实际存放元素大小size(S)的关系

  • 注意capacity变量不是成员变量,而是实际存放数据数组的长度,可以理解成table.length

  • T = C * L

  • 当S>T时会触发扩容操作,此时C会变成原来的2倍(当数组长度C 是2的 n 次时, hash&(length-1) == hash%length,因为&操作比%操作效率高,所以数组长度C总是2的n次方,目的是为了提升效率。)举例:默认大小C为16,此时T=16 * 0.75 = 12,当S=13时,触发扩容,C=C2=162=32,T=32 * 0.75=24

put方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
   //定义tab p n i          
   Node<K,V>[] tab; Node<K,V> p; int n, i;
   
   //第一次往map里面put时候会调用扩容resize方法初始化table
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;//初始化table并且将长度赋值给n
   
   //i = (n - 1) & hash 会得到当前元素放置在数组中的位置,
   //和hash % n的值相等(前提是table.length为2的幂次方),但是&的操作效率更高
   //如果该位置上面没有元素则直接新建一个元素
   if ((p = tab[i = (n - 1) & hash]) == null)
       tab[i] = newNode(hash, key, value, null);
     
   //下面的操作在该元素位置上有值的时候进行操作    
   else {
       Node<K,V> e; K k;
       //如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e
       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;
      }
  }
   //结构化修改加1
   ++modCount;
   
   //如果实际所装的元素大于了阈值,则触发扩容操作
   if (++size > threshold)
       resize();
       
   afterNodeInsertion(evict);
   return null;
}

get方法

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;
   
   //first = tab[(n - 1) & hash] 通过(n - 1) & hash定位到该键所在桶的索引
   //如果桶为null则直接返回null
   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);
      }
  }
   //没找到或各种意外情况下都返回null
   return null;
}

JDK1.7

JDK1.7中的put和get方法就简单很多,也没有红黑树那些转换

put

public V put(K key, V value) {
   //没有初始化的时候先初始化
   if (table == EMPTY_TABLE) {
       inflateTable(threshold);
  }
   //空key的时候
   if (key == null)
       return putForNullKey(value);
   
   //计算hash    
   int hash = hash(key);
   //计算出该hash在当前桶中的定位
   int i = indexFor(hash, table.length);
   
   //如果桶是一个链表,则循环链表
   for (Entry<K,V> e = table[i]; e != null; e = e.next) {
       Object k;
       //如果在当前链表中找到键相同的,则替换掉原来的值
       if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
           V oldValue = e.value;
           e.value = value;
           e.recordAccess(this);
           //返回原来的值
           return oldValue;
      }
  }
   //如果桶是空的或者没在链表中找到一样的键则新增加一个Entry
   modCount++;
   addEntry(hash, key, value, i);
   return null;
}


void addEntry(int hash, K key, V value, int bucketIndex) {
   //如果实际的容量达到了阈值,则需要扩容
   if ((size >= threshold) && (null != table[bucketIndex])) {
       //扩容为原来的两倍
       resize(2 * table.length);
       hash = (null != key) ? hash(key) : 0;
       //在计算出该key的索引,即在桶中的位置
       bucketIndex = indexFor(hash, table.length);
  }
   //创建Entry
   createEntry(hash, key, value, bucketIndex);
}


void createEntry(int hash, K key, V value, int bucketIndex) {
   //把当前位置上的元素拿出来
   Entry<K,V> e = table[bucketIndex];
   //新建Entry,如果e不是null的话,则形成链表结构
   table[bucketIndex] = new Entry<>(hash, key, value, e);
   size++;
}

get

public V get(Object key) {
   if (key == null)
       return getForNullKey();
   Entry<K,V> entry = getEntry(key);
   return null == entry ? null : entry.getValue();
}


final Entry<K,V> getEntry(Object key) {
   if (size == 0) {
       return null;
  }
   //计算hashCode
   int hash = (key == null) ? 0 : hash(key);
   
   //indexFor(hash, table.length) 计算出在桶中的位置
   //如果是链表则循环找到键相同的Entry
   for (Entry<K,V> e = table[indexFor(hash, table.length)];
        e != null;
        e = e.next) {
       Object k;
       if (e.hash == hash &&
          ((k = e.key) == key || (key != null && key.equals(k))))
           return e;
  }
   
   //如果啥也没找到,则直接返回null
   return null;
}


以上是关于JDK源码HashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章

2JDK8中的HashMap实现原理及源码分析

HashMap源码分析--jdk1.8

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

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

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

JDK1.8源码分析之HashMap