HashMap底层源码解析(详细)

一、HashMap简介

1.HashMap

​ 基于哈希表的实现的Map接口。此实现提供了所有可选的地图操作,并允许null的值和null键。( HashMap类大致相当于Hashtable ,除了它是不同步的,并允许null)。这个类不能保证地图的顺序;特别是,它不能保证订单在一段时间内保持不变。假设哈希函数在这些存储桶之间正确分散元素,这个实现为基本操作( get和put )提供了恒定的时间性能。 收集视图的迭代需要与HashMap实例(桶数)加上其大小(键值映射数)的“容量” 成正比 。 因此,如果迭代性能很重要,不要将初始容量设置得太高(或负载因子太低)是非常重要的。

2.重要参数

​ HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的桶数,初始容量只是创建哈希表时的容量。负载因子是在容量自动增加之前允许哈希表得到满足的度量。当在散列表中的条目的数量超过了负载因数和电流容量的乘积,哈希表被重新散列(即,内部数据结构被重建),使得哈希表具有桶的大约两倍。作为一般规则,默认负载因子(.75)提供了时间和空间成本之间的良好折中。更高的值会降低空间开销,但会增加查找成(反映在HashMap类的大部分操作中,包括get和put )。在设置其初始容量时,应考虑地图中预期的条目数及其负载因子,以便最小化重新组播操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新排列操作。 如果许多映射要存储在HashMap实例中,则以足够大的容量创建映射将允许映射的存储效率高于使其根据需要执行自动重新排序以增长表。请注意,使用同一个hashCode()多个密钥是降低任何哈希表的hashCode()的一种方法。

3.同步机制

请注意,此实现不同步。 如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,那么它必须在外部进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅改变与实例已经包含的密钥相关联的值不是结构修改。)这通常通过对自然地封装映射的一些对象进行同步来实现。 如果没有这样的对象存在,地图应该使用Collections.synchronizedMap方法“包装”。 这最好在创建时完成,以防止意外的不同步访问地图:

  Map m = Collections.synchronizedMap(new HashMap(...)); 

4.迭代器

​ 由所有此类的“collection 视图方法”所返回的迭代器都是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove方法,而不冒在将来不确定的时间发生任意不确定行为的风险。

**注意:**迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

5.组成

​ JDK1.8之前HashMap由:数组+链表组成;数组是HashMap的主体,链表则是主要为了解决哈希冲突(两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引相同),当链表长度大于阈值(或者红黑树的边界值,默认为8),并且当前数组的长度大于64时,此时索引位置上的所有数据改为使用红黑树存储(数组+链表+红黑树)。

补充:将链表转换为红黑树会判断,即使阈值大于8.但是数组长度小于64,此时并不会将链表变为红黑树,而是进行数组扩容。

阈值大于8并且数组长度大于64时,链表转换为红黑树时效率更高效

6.HashMap特点:

  • 存取无序
  • 值和值位置都可以是null,但是键位置只能有一个null
  • 键位置是唯一的,底层的数据结构控制Key的位置
  • JDK1.8前的数据结构:链表+数组 JDK1.8之后:链表+数组+红黑树
  • 阈值(边界值)>8并且数组长度大于64才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

二、HashMap集合底层的数据结构

1.存储规则说明

  • HashMap hashMap = new HashMap<>(16);

    创建HashMap对象后,在JDK1.8之前,底层创建了一个长度为16的Entry[] table,在1.8之后没有在创建集合对象的时候创建数组,而是在首次调用put方法时创建长度为16的Node[] table。

  • 假设向哈希表中存储数据Hash为12,即索引1,根据Hash调用String类中的hashCode()方法计算出哈希码值,该哈希码值经过某种算法计算以后得到在Node数组中存放的位置,如果此位置上数据为空直接添加到该位置。否则开始比对二者的哈希值(即Hash和胡歌),如果哈希值不相同,那么胡歌就会在此空间划出一个节点变成一个链表来存储。以此类推,后继续添加地瓜花,(即比较胡歌和地瓜花)哈希值不相同时地瓜花就会再划分出一个节点进行添加。**注意:**哈希值相等时对之前的数据进行替换,即替换旧的value。

    如果两个key的hashcode相同,通过equals比较内容是否相同,如果相同则新value覆盖旧的value,否则将新的键值添加到哈希表中。

HashMap<String, Integer> hashMap = new HashMap<>(16);
        hashMap.put("Hash",12);
        hashMap.put("Map",13);
        hashMap.put("陈冠希",14);
        hashMap.put("吴彦祖",15);
        hashMap.put("胡歌",16);
        hashMap.put("地瓜花",17);

HashMap底层源码解析(详细)_第1张图片

2.HashMap的put(K key,V value)方法

HashMap底层源码解析(详细)_第2张图片

3.HashMap的继承关系

HashMap底层源码解析(详细)_第3张图片

4.HashMap的初始容量

private final int DEFAULT_INTIAL_CARACITY = 1<<4

默认初始容量为1<<4 = 16,HashMap的容量必须是2的n次幂

  • 例:为2的n次幂—>均匀分配空间

    假设数组长度为8 ------------  hash&(length-1) = hash % length
    8----0000 10001->0000 0001
        	 0000 01118-10000 0001-->12->0000 0010
        	 0000 0111
        	 0000 0010-->23->0000 0011
        	 0000 0111
        	 0000 0011-->34->0000 0100
        	 0000 0111
        	 0000 0100-->45->0000 0101
        	 0000 0111
        	 0000 0101-->56->0000 0110
        	 0000 0111
        	 0000 0110-->1
    
  • 反例:不为2的n次幂---->分配空间不均匀

    假设数组长度为8 
    7----0000 00001->0000 0001
        	 0000 0110(7-1)
        	 0000 0000-->02->0000 0010
        	 0000 0110
        	 0000 0010-->23->0000 0011
        	 0000 0110
        	 0000 0010-->24->0000 0100
        	 0000 0110
        	 0000 0100-->45->0000 0101
        	 0000 0110
        	 0000 0100-->46->0000 0110
        	 0000 0110
        	 0000 0110-->6
    

    如果不考虑效率,直接求余数就不需要长度是2的n次幂

总结:

  • 如果数组长度为2的n次幂可以保证数据的均匀插入,如果不是n次幂,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大了哈希碰撞。
  • 我们一般会想到通过取余(%)的方式来确认位置,这样也是可以的,但是性能不如位与(&)运算,而且数组长度为2的n次幂时,hash & (length-1) = hash % length。
  • HashMap容量为2的n次幂是为了数据的均匀分布,减少哈希冲突。哈希冲突越大代表数组中的一个链表越长,降低HashMap的性能。
  • 在创建HashMap对象时容量不为2的n次幂,HashMap会通过一系列的位移运算和或运算得到一个2的n次幂,即离我们指定容量最近的数字。
/**
* 用指定的容量和负载因子初始化一个HashMap
* @param initialCapacity 初始容量
* @param loadFactor 负载因子
*/
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);
}

一般使用:
/**
* 指定容量初始化一个HashMap,负载因子默认为0.75
*/
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

计算下标:

/**
* 计算容量
* 让HashMap的容量永远是2的n次幂
*/    
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;
}
  • cap-1操作:防止cap已经是2的n次幂。如果cap已经是2的n次幂则返回的capacity将是这个数的两倍!

    • 例:不减1的的情况:当cap=8
    /**
     * 返回HashMap的容量
     * @param cap int
     * @return 返回HashMap的容量
     */
    public static int tableSizeFor(int cap){
        System.out.println("cap=" + cap+"---->"+getBinary(cap));
        int n = cap ;
        n |= n >>> 1; 
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        System.out.println("n=" + n+"---->"+getBinary(n));
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
    /**
     * int转换为二进制
     * @param cap int值
     * @return 返回二进制
     */
    public static String getBinary(int cap){
        String result = "";
        LinkedList<String> list = new LinkedList<>();
        while(cap != 0) {
            if(cap % 2 == 0) {
                list.addFirst("0");
            }else {
                list.addFirst("1");
            }
            cap/=2;
        }
        for(int i=0;i<8-list.size();i++){
            result += "0";
        }
        for (String string:list){
            result+=string;
        }
        return result;
    }
    

    ​ 计算结果为:
    HashMap底层源码解析(详细)_第4张图片

    • 例:减1的情况:当cap=8

      计算结果为:
      HashMap底层源码解析(详细)_第5张图片

  • 分析tableSizeFor(int cap)

    public static int tableSizeFor(int cap){
        int n = cap - 1;
    //        例:当cap=12  n=11
    //        n |= n >>> 1; 第一次位移
    //        00000000 00000000 00000000 00001011
    // 右移    00000000 00000000 00000000 00000101
    //或操作   00000000 00000000 00000000 00001111   ===15
    
    //        此时 n =15
    //        n |= n >>> 2; 第一次位移
    //        00000000 00000000 00000000 00001111
    // 右移得  00000000 00000000 00000000 00000111
    //或操作得 00000000 00000000 00000000 00001111   ===15
    //        注意:容量最大为32的正数,因此最后只有 n |= n >>> 16
        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;
    }
    

执行多次位移运算和或运算的目的:为了将低位全部变成1

最后返回n+1操作使我们最终计算的结果为2的n次幂

最后将计算出来的容量值赋给 this.threshold

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);
}

5.其他的变量属性

/*
* 负载因子
* 当HashMap中元素的个数 >= 数组长度*负载因子就会扩容数组。
* 负载因子在使用的过程中,不建议使用;即次此构造函数一般不使用。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/*
* TreeNode的临界值
* 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度是log(8) = 3;
* 链表的平均查找长度为n/2,如果长度为8的情况下,8/2=4,即效率低于红黑树,所以需要转换为红黑树;
*/
static final int TREEIFY_THRESHOLD = 8;  //长度大于8时链表转为红黑树

/*
* 集合最大容量
* 二进制表示:01000000 00000000 00000000 00000000
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/*
* 当链表的值小于6时从红黑树转回链表
* 链表长度等于6, 6/2=3,而log(6) ≈ 2.6,虽然比链表快,但是效率差距并不大,而且链表转换为红黑树也需* 要一定的时间,所以这时候并不会转换为红黑树。
*/
static final int UNTREEIFY_THRESHOLD = 6; //长度小于6时红黑树转为链表

/*
* 当Map中的数量超过这个值,表中的桶才能树化,否则元素过多时只是扩容。
*/
static final int MIN_TREEIFY_CAPACITY = 64;

/*
* table用来初始化(必须是二的n次幂)
*/
transient Node<K,V>[] table;

/*
* 存放缓存
*/
transient Set<Map.Entry<K,V>> entrySet;

/*
* HashMap中存放元素的个数
* 标识HashMap中key-value的数量,而不是数组的长度
*/
transient int size;

/*
* 用来记录HashMap的修改次数
*/
transient int modCount;

/*
* 用来调整大小下一个容量的值计算方式为(容量*负载因子)
*/
int threshold;

/*
* 哈希表的加载因子(重点)
* 1.loadFactor负载因子,是用来衡量HashMap满的程度,标识HashMap的疏密程度,影响hash操作到同一个数* 组位置的概率,计算HashMap的实时加载因子的方法为:size / capacity。
* 2.loadFactor太大会导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。0.75是官方经过  * 大量的数据测试,得出的最好的数字。
* 3.当HashMap中容纳的元素超过边界值时,认为HashMap太挤了,就需要扩容。这个扩容的过程涉及到rehash、
* 复制等操作,非常的消耗性能,所以开发中尽量减少扩容的次数,可以通过创建HashMap时指定初始化容量来尽量* 的避免
*/
final float loadFactor;

6.构造方法

1. 无参构造:默认负载因子为0.75f

  • 在jdk7中,new HashMap时就创建了Hash桶

  • 在jdk8中,new HashMap时并不会创建数组,而是在put方法使用时先判断table是否为空然后创建数组

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2. 有参构造1:指定容量和默认负载因子的HashMap(开发中建议使用)

public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

3. 有参构造2:指定容量和负载因子的HashMap(开发中不建议使用)

public HashMap(int initialCapacity, float loadFactor) {
    // 判断初始化容量     
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 判断初始容量是否大于最大初始容量  
    if (initialCapacity > MAXIMUM_CAPACITY)
	// 将最大容量赋给initialCapacity
        initialCapacity = MAXIMUM_CAPACITY;
	//加载因子是否小于等于0 或者 是一个非法数值NaN
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    // 将指定的负载因子赋值给loadFactor
    this.loadFactor = loadFactor;
    // tableSizeFor判断指定的初始化容量是否为2的n次幂。如果不是便会转为比此容量的最小2的n次幂。
    // 将计算出来的2的n次幂数值赋给边界值
    this.threshold = tableSizeFor(initialCapacity);
}

4. 参数为Map的构造方法

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) {
    //获取map集合的长度
    int s = m.size();
    if (s > 0) {
        // 判断table是否已经初始化
        if (table == null) { // pre-size
            // 初始化
            // +1.0F:相当于给小数向上取整,尽可能保证更大容量,能够减少resize次数
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            // 判断t的值是否大于阈值,如果大于阈值则初始化阈值
            if (t > threshold)
                threshold = tableSizeFor(t);
            }
        else if (s > threshold)
            // 已经初始化,并且元素个数大于阈值,进行扩容
            resize();
        // 将m中的所有元素添加到HashMap中
        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);
        }
    }
}

7.成员方法

1. put方法

  • 分析:
    • 1.先通过hash值计算出key映射到哪个桶;
    • 2.如果桶上没有碰撞冲突,则直接插入;
    • 3.如果出现碰撞冲突了,则需要处理冲突:
      • a:如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
      • b:否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树。
    • 4.如果桶中存在重复的键,则为该键替换新值value;
    • 5.如果size大于阈值threshold,则进行扩容;
public V put(K key, V value) {
    // 调用了putVal方法
    return putVal(hash(key), key, value, false, true);
}

/**
    * key == null 返回0
    * key != null 计算出hashCode赋给h,接着h进行无符号右移16位再进行异或运算
       假设 h = key.hashCode() 计算得出的结果为
       1111 1111 1111 1111 1111 1100 1010  //调用hashCode计算出来的结果
       h >>> 16
       0000 0000 0000 0000 1111 1111 1111  //h右移16位
       异或运算:
       1111 1111 1111 1111 1111 1100 1010
       0000 0000 0000 0000 1111 1111 1111
       1111 1111 1111 1111 0000 0011 0101 //得到hash值
       (length - 1)& hash
       假设length = 16,则length-1=15
       0000 0000 0000 0000 0000 0000 1111   === >15
       1111 1111 1111 1111 0000 0011 0101
       0000 0000 0000 0000 0000 0000 0101  ===> 下标结果为5

        结论:高16位不变;高16位和低16位进行了一个异或运算。
        原因:如果当length长度很小,假设为16,那么length-1=15即1111,按位与运算只使用了hash值得			后四位,高位失去了意义;当哈希值高位变化很大,地位变化很小,易造成哈希冲突,所以要把高位			和地位都利用起来。解决了此问题。(防止哈希碰撞)
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


/**
    * @param key的Hash值
    * @param 原来的key
    * @param key对应的value
    * @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(数组)是否为空
    // 将table赋值给tab并判断是否为null
    // 将tab的长度赋值给n并判断是否等于0
    if ((tab = table) == null || (n = tab.length) == 0)
        // resize实例化一个数组
        n = (tab = resize()).length;
    // 位置上没有值
    // i =(n-1) & hash 计算当前key所在的下标在哪个tab(桶)中并将下标赋值给i
    // p = tab[i] 表示将该位置的值赋值给P并判断是否为null
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 创建一个Node元素,赋值给当前下标位置
        tab[i] = newNode(hash, key, value, null);
    else {
        // 位置上已经有值的情况
        Node<K,V> e;
        K k;
        // p在55行已经被赋了值,可以直接进行操作
        // 判断之前算出的p值是否和传进来的hash相同并且这个key==k(即p.key)
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            // 说明当前位置元素的key和我们传进来的key是相等的
            e = p;
        // 判断是否为红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 不是红黑树,插入的key和当前下标的key不相等,遍历链表
        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;
                }
                // 判断key.equals(e.key) ? break
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))){
                    break;
                }
                // 说明新添加的节点和当前节点不相同,继续找下一节点的元素
                p = e;
            }
        }
        if (e != null) { // e不为空,说明前边已经找到了可以存储key-value的节点Node
            // 取出旧值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                // 新值赋值
                e.value = value;
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 统计数据修改次数
    ++modCount;
    // 大于临界值的情况
    if (++size > threshold)
        // 调整容量
        resize();
    afterNodeInsertion(evict);
    return null;
}

^ :异或运算---->数字相同为0,否则为1

2. remove方法

/**
* 根据key删除元素
*/
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

/**
* 删除方法
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    Node<K,V>[] tab;
    Node<K,V> p;
    int n, index;
    // 获取table赋值给tab,判断tab不为null;获取tab的长度赋值给n,判断n>0
    // 根据hash获取索引位置,赋值给index,取出index位置的元素赋值给p并判断p不为null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        // hash桶不为空并且当前key所在的位置的元素不为空
        Node<K,V> node = null, e;
        K k;
        V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 第一个位置的元素就是我们要查找的元素
            node = p;
        // 获取p的下一个节点赋值给e,并判断e是否为null
        else if ((e = p.next) != null) {
            // 判断是否为红黑树
            if (p instanceof TreeNode)
                // 直接获取红黑树节点
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
           else {
               // 遍历链表
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 判断node不为空(上边的操作已经找到了节点) 
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 是否为红黑树
            if (node instanceof TreeNode)
                // 移除树节点
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 说明node是第一个节点,直接将下一节点赋值给当前下标
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

3. get方法

/**
* 根据key获取value
*/
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
* 获取value
*/
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab;
    // first用来存放对应下标位置的第一个元素
    Node<K,V> first, e;
    int n;
    K k;
    // 获取table赋值给tab并判断tab!=null;获取tab的长度估值给n并判断n>0;
    // 根据传进来的key计算下标位置取出该下标位置的元素赋值给first并且判断first!=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;
        // 获取下一个节点赋值给e并判断e!=null
        if ((e = first.next) != null) {
            // 是否为红黑树
            if (first instanceof TreeNode)
                // 获取红黑树节点
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 遍历链表,知道下一节点为null
            do {
                // 在链表中找到了对应key位置的元素,则返回e
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

8.数组扩容

  • 什么时候扩容?

    当HashMap中元素个数超过数组长度*负载因子(0.75f)的时候会进行扩容。扩容方式就是把数组扩大2倍。然后重新计算每个元素在数组中的位置(消耗性能,尽量减少扩容操作).

  • HashMap中的扩容是什么?

    jdk1.8之前: 使用resize会重新遍历hash中所有的元素计算hsah值,此操作耗性能,所以要尽量避免hash的扩容

    jdk1.8之后:使用rehash每次扩容都是翻倍,与原来的hash&(n-1)的结果相比只是多了一个二进制位,所以节点要么在原来的位置,要么在 原位置+原容量

    例如:从16扩容到32

    HashMap底层源码解析(详细)_第6张图片

resize函数
/*
* 数组扩容
*/
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) {
            // Integer的最大值赋给临界值
            threshold = Integer.MAX_VALUE;
            // 返回旧数组
            return oldTab;
        }
        // 没超过最大容量,数组扩容为原来的2倍然后赋值给新的容量并判断是否小于最大容量
        // 原数组长度等于
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 当前容量在默认值和最大值的一半之间
            // 新的临界值为当前临界值的2倍
            newThr = oldThr << 1; // X2
    }
    // 旧的临界值大于0
    else if (oldThr > 0)
        newCap = oldThr;
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的临界值等于0的情况
    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 {
                    // 表示该位置为链表
                    /*
                    * loHead:表示旧的头节点 loTail:表示旧的数据链表
                    * hiHead:表示新的头节点 hiTail:表示新的数据链表
                    */
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 表示这个节点在resize之后不需要移动位置
                        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;
}

9.Map的遍历方法

1.分别遍历key和value

/**
 * 分别遍历key和value
 */
@Test
public void testMap1(){
    HashMap<String,Integer> map = getMap();
    // 获取key
    for (String key : map.keySet()){
        System.out.println(key);
    }
    // 获取value
    for (Integer value : map.values()) {
        System.out.println(value);
    }
}

2.使用迭代器

/**
 * 使用迭代器
 */
@Test
public void testIterator(){
    HashMap<String, Integer> map = getMap();
    Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
    while(iterator.hasNext()) {
        Map.Entry<String, Integer> entry = iterator.next();
        System.out.println(entry.getKey()+":"+entry.getValue());
    }
}

3.通过get方式遍历(不建议使用)

/**
 * 通过get方式遍历(不建议使用)
 * 不建议使用的原因:因为要迭代多次:KeySet一次,get一次。耗性能
 */
@Test
public void testGet(){
    HashMap<String, Integer> map = getMap();
    Set<String> keySet = map.keySet();
    for (String key :
            keySet) {
        System.out.println(key+":"+map.get(key));
    }
}

4.JDK1.8以后使用Map接口中的一个默认方法

/**
 * JDK1.8以后使用Map接口中的一个默认方法
 */
@Test
public void testForeach(){
    HashMap<String, Integer> map = getMap();
    map.forEach((key,value)->{
        System.out.println(key+":"+value);
    });
}

三、红黑树

​ 是一种自平衡的二叉查找树,每个节点都带有颜色属性,红色或黑色。

1.特性:

  • 具有二叉查找树的特性;
  • 节点是红色或者是黑色;
  • 根节点一定是黑色的;
  • 每一个叶子节点(NIL节点)都是黑色的;
  • 每个红色节点的两个子节点都是黑色的(从每个几点到根的所有路径上不可能有两个连续的红色节点);
  • 从任意一个几点到其每个叶子节点的所有路径包含相同数目的黑色节点。

2.红黑树的查找

/**
 * Calls find for root node.
 */
final TreeNode<K,V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);
}

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

/**
 * Finds the node starting at root p with the given hash and key.
 * The kc argument caches comparableClassFor(key) upon first use
 * comparing keys.
 */
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir;
        K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if (pl == null)
            p = pr;
        else if (pr == null)
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
    return null;
}

3.将链表转换为红黑树

/**
 * 替换指定hash表的索引出桶中的所有节点,除非表太小,否则修改大小
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index;
    Node<K,V> e;
    // 如果数组为null,或则数组长度小于进行树形化的阈值(64)就去扩容,而不是转换为红黑树;如果数组很小时转换为红黑树然后遍历效率更低一些,所以进行扩容并重新计算hash值,链表的长度便会变短
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 通过当前key的hash计算当前key所在的下标位置,取出赋值给e并判断不为null
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //说明阈值大于8且数组长度大于64
        //hd:红黑树的头节点  tl:红黑树的尾节点
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 重新创建树节点,内容和当前链表节点e一致
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                // 将新创建的p节点赋值给红黑树的头节点
                hd = p;
            else {
                // 将上一个节点赋给现在的p的前一个节点
                p.prev = tl;
                // 将现在的节点p作为树的尾节点的下一个节点
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
       //让桶中的第一个元素即数组中的元素指向新的红黑树节点,以后这个桶里的元素就是红黑树,而不是链表
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

4.左旋、右旋、(put,remove方法用到)变色

  • 左旋:以某个节点作为支点,其右节点变为旋转节点的父节点,右节点的左节点变为旋转节点的右节点,其余不变;
  • 右旋:以某个节点作为指点,其左节点变为旋转节点的父节点,左节点的右节点变为旋转节点的左节点,其余不变;
  • 变色:节点颜色由红变黑或黑变红的过程

四、面试题:

1.HashMap中的hash函数是如何实现的?还有哪些hash函数的实现方式?

  • 对key的hashCode做hash操作,无符号右移16位做异或运算(即计算下标;效率最高)。
  • 取余数法、伪随机数法等(效率都比较低)
  • 按位与运算:hash&(length-1)

2.当两个对象的hashCode相等时会怎么样?

  • 会产生哈希碰撞,若key值相等时会替换旧的value,否则链接到链表后方,链表长度超过阈值8,并且数组长度大于64就会转为红黑树。

3.何时会发生哈希碰撞?

  • 只要两个元素的key计算的哈希值相同,就会发生哈希碰撞。

4.如何解决哈希碰撞?

  • JDK1.8之前使用链表
  • JDK1.8之后使用链表+红黑树

5.如果两个键的hashcode相同,如何存储键值对?

  • hashcod相同,通过equals方法比较内容是否相同,如果相同,新的value会覆盖旧的value,否则新的value添加到链表中。

你可能感兴趣的:(Java)