HashMap,你了解多少?

概述

上一篇文章 Android 中高级面试必知必会 中 JAVA 部分的第一部分就是容器,容器是 JAVA 中很是重要的一个部分,也是面试时考察基础知识很重要的一环,咱们首先来看下图,关于容器部分的整体框架html

集合框架主要分为两大类: Collection 和 Map。java

Collection 是 List、Set 等集合高度抽象出来的接口,它包含了这些集合的基本操做,它主要又分为两大部分:List和Set。node

Map 是一个映射接口,其中的每一个元素都是一个 key-value 键值对,一样抽象类 AbstractMap 经过适配器模式实现了 Map 接口中的大部分函数。像咱们经常使用的 HashMap、LinkedHashMap 都是继承自 Map 接口。面试

今天咱们主要讲解容器中的 HashMap 部分。算法

问题

咱们首先来看下面几个问题,读者能够本身试着回答一下。
一、 HashMap 的实现原理?底层数据结构?
二、 HashMap 的扩容,扩容因子?
三、 什么是哈希碰撞,如何解决哈希碰撞?
四、 HashMap 是线程安全的吗?shell

HashMap

要讲到 HashMap,咱们主要分为如下主要模块进行讲解。编程

哈希

Hash ,通常直接音译为“哈希”或“散列”,就是把任意长度的输入,经过散列算法,变化为固定长度的输出,输出值则称为散列值。数组

常见的 Hash 函数有一下几种。安全

  1. 直接定址法:直接以关键字 k 或者 k 加上某个常数做为哈希地址。
  2. 数字分析法:提取关键字中比较均匀的数字做为哈希地址。
  3. 除留余数法:用关键字 k 初一某个不大于哈希表长度 m 的 数 p ,将所得余数做为哈希表地址。
  4. 伪随机数法:采用一个伪随机数看成哈希函数。

哈希碰撞

两个不一样的输入值,通过同一散列函数计算出的散列值相同的现象叫作哈希碰撞。bash

常见的解决哈希碰撞的方法有以下几种:

  1. 开放地址法:一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址就可以找到,并将记录存入。
  2. 链地址法:将哈希表的每一个单元做为链表的头结点,全部哈希地址为 i 的元素构成一个同义词链表。即发生冲突时,就把该关键字链放在以该单位为头结点的链表的尾部。
  3. 再哈希法:当哈希地址出现冲突后,用其余函数计算另外一个哈希函数的地址,直到冲突再也不产生为止。
  4. 创建公共溢出区:将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。

实现原理

在 JDK1.6,JDK1.7 中,HashMap 采用数组+链表实现,即便用链表处理冲突,同一 hash 值的链表都存储在一个链表里。可是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,经过 key 值依次查找的效率较低。

而 JDK1.8 中,HashMap 采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减小了查找时间。

扩容

加载因子(默认0.75):为何须要使用加载因子,为何须要扩容呢?
由于若是填充比很大,说明利用的空间不少,若是一直不进行扩容的话,链表就会愈来愈长,这样查找的效率很低,由于链表的长度很大(固然最新版本使用了红黑树后会改进不少),扩容以后,将原来链表数组的每个链表分红奇偶两个子链表分别挂在新链表数组的散列位置,这样就减小了每一个链表的长度,增长查找效率。

构造 hash 表时,若是不指明初始大小,默认大小为 16(即 Node 数组大小 16),若是 Node[] 数组中的元素达到(填充比 * Node.length)从新调整 HashMap 大小 变为原来 2 倍大小,扩容很耗时

/** 
    * 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;  
           }  
/*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/  
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
                    oldCap >= DEFAULT_INITIAL_CAPACITY)  
      /*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/  
               newThr = oldThr << 1; // double threshold  
       }  
    /*若是旧表的长度的是0,就是说第一次初始化表*/  
       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;//把新表赋值给table  
       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)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置  
                       newTab[e.hash & (newCap - 1)] = e;  
                   else if (e instanceof TreeNode)  
                       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
/*若是e后边有链表,到这里表示e后面带着个单链表,须要遍历单链表,将每一个结点重*/  
                   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;//记录下一个结点  
          //新表是旧表的两倍容量,实例上就把单链表拆分为两队,  
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对  
                           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) {//lo队不为null,放在新表原位置  
                           loTail.next = null;  
                           newTab[j] = loHead;  
                       }  
                       if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置  
                           hiTail.next = null;  
                           newTab[j + oldCap] = hiHead;  
                       }  
                   }  
               }  
           }  
       }  
       return newTab;  
   }
复制代码

HashMap 的 get 方法

/** 
    * 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;  
           }  
/*把新表的长度设置为旧表长度的两倍,newCap=2*oldCap*/  
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
                    oldCap >= DEFAULT_INITIAL_CAPACITY)  
      /*把新表的门限设置为旧表门限的两倍,newThr=oldThr*2*/  
               newThr = oldThr << 1; // double threshold  
       }  
    /*若是旧表的长度的是0,就是说第一次初始化表*/  
       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;//把新表赋值给table  
       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)//说明这个node没有链表直接放在新表的e.hash & (newCap - 1)位置  
                       newTab[e.hash & (newCap - 1)] = e;  
                   else if (e instanceof TreeNode)  
                       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
/*若是e后边有链表,到这里表示e后面带着个单链表,须要遍历单链表,将每一个结点重*/  
                   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;//记录下一个结点  
          //新表是旧表的两倍容量,实例上就把单链表拆分为两队,  
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对  
                           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) {//lo队不为null,放在新表原位置  
                           loTail.next = null;  
                           newTab[j] = loHead;  
                       }  
                       if (hiTail != null) {//hi队不为null,放在新表j+oldCap位置  
                           hiTail.next = null;  
                           newTab[j + oldCap] = hiHead;  
                       }  
                   }  
               }  
           }  
       }  
       return newTab;  
   }
复制代码

HashMap 中的 put 方法

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; /*若是table的在(n-1)&hash的值是空,就新建一个节点插入在该位置*/ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); /*表示有冲突,开始处理冲突*/ else { Node<K,V> e; K k; /*检查第一个Node,p是否是要找的值*/ 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); //若是冲突的节点数已经达到8个,看是否须要改变冲突节点的存储结构,&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//treeifyBin首先判断当前hashMap的长度,若是不足64,只进行 //resize,扩容table,若是达到64,那么将冲突的存储结构为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /*若是有相同的key值就结束遍历*/ if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /*就是链表上有相同的key值*/ if (e != null) { // existing mapping for key,就是key的Value存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;//返回存在的Value值 } } ++modCount; /*若是当前大小大于门限,门限本来是初始容量*0.75*/ if (++size > threshold) resize();//扩容两倍 afterNodeInsertion(evict); return null; } 复制代码

HashMap 是否为线程安全?

关于 HashMap 线程不安全这一点,《Java 并发编程的艺术》一书中是这样说的

HashMap 在并发执行 put 操做时会引发死循环,致使 CPU 利用率接近 100%。由于多线程会致使 HashMap 的 Node 链表造成环形数据结构,一旦造成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。

其实死循环并非发生在put操做时,而是发生在扩容时。详细的解释能够看下面几篇博客:
Java HashMap 的死循环
HashMap在java并发中如何发生死循环

如何线程安全的使用 HashMap?

  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
  
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
  
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
复制代码

具体实现原理在后续文章中会有所涉猎,请持续关注。

以后会就行更新 Java 中的集合相关内容,我会根据内容多少决定分几篇文章去讲,大体内容如我整理脑图

为避免失联或想第一时间查看个人文章更新,可关注个人微信公众号 KevenZheng ,以后会陆续更新上述目录的内容,敬请关注。

如需转载,请联系我或注明出处!

相关文章
相关标签/搜索