当前位置: 首页 > 工具软件 > has > 使用案例 >

Has和Map的底层原理与扩容机制

许招
2023-12-01

HashMap 的底层实现原理

本文参考javaguide提供的面试题,可以了解详情javaguide

同时参考了其他博主的文章了解详情

简介

HashMap是用来存储键值对的一种集合,它基于哈希表的Map接口实现

HashMap继承与AbstractMap类和实现Map接口

HashMap在jdk1.7之前是由数组加链表组成的,在jdk1.8之后为了提高查询效率增加了红黑树这种数据结构。

当链表长度大于阈值(默认为8)和HashMap的数组长度超过64的时候就会使用到红黑树。

底层分析

1.HashMap通过key的hashcode经过扰动函数的处理后得到Hash值。

通过(n-1)&hash 判断当前元素存储位置(n为数组长度)。

如果当前位置有元素的话,就要判断存入的元素的hash值以及key是否相同,如果相同的话就直接覆盖,如果不同就通过拉链法解决冲突。

拉链法就是将链表和数组将结合,也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

扰动函数就是HashMap中的hash方法,防止一些比较差的hashcode方法减少碰撞。

(key的hashcode)和(这个hashcode无符号右移16位)的异或后的值;

(h = key.hashCode()) ^ (h >>> 16);

2.当链表的长度大于阈值8的时候,会首先调用treeifyBin方法,这个方法会根据HashMap数组来决定是否转换为红黑树,只有当数组的长度大于64的时候才会转化为红黑树来处理。否则就是执行resize方法来进行数组的扩容。

Map在使用过程中不断的往里添加数据,当数据达到了16*0.75=12(16是默认容量0.75是加载因子的默认值)的时候就会对当前的16考虑扩容。而扩容需要涉及到rehash,复制数据等操作,所以非常消耗性能。

HashMap的4种构造方法

1.默认构造函数

2.包含一个Map的构造函数

3.指定容量大小的构造函数

4.指定容量大小和加载因子的构造函数

put方法

HashMap中提供put方法添加元素,它是由putVal实现的。

1.putVal方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

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未初始化或者长度为0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (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;
        // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 将第一个元素赋值给e,用e来记录
                e = p;
        // hash值不相等,即key不相等;为红黑树结点
        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 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict)

1.key的hash值

2.要存入的key值

3.要存入的value值

4.onlyIfAbsent 如果当前位置已存在一个值,是否替换,false是替换,true是不替换

5.evict 表是否在创建模式,如果为false,则表是在创建模式。

执行流程

1.判断table是否为空或者长度为0,如果为空则使用resize扩容

2.检查table中的(n - 1) & hash位置是否有值,没有则直接插入到数组中

3.如果桶中的存在的元素的key和hash,则直接覆盖value

4.判断当前存放这个元素是否存到红黑树中,直接插入,上面3

不成立,则hash值不相等,key值不相等

5.当3和4都不成立,将输入的元素存放在链表中

6.存在key值和hash值相等的,直接覆盖旧value

7.将记录修改次数+1,判断是否需要扩容,如果需要就扩容。

size>=threshold 当前数量>=初始容量Capacity*加载因子loadFactor=12

get方法

get方法中使用getNode方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    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) {
            // 在树中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在链表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

执行过程

1.判断table中是否为空,且长度大于0

2.且该hash值对应的table元素不为空。

3.条件成立则判断该节点的哈希值是否等于hash,依次遍历链表或者红黑树,返回查找到的节点的value

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) {
            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
        newCap = oldThr;
    else {
        // signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 计算新的resize上限
    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) {
        // 把每个bucket都移动到新的buckets中
        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 {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 原索引
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

HashMap扩容可以分为三种情况:

第一种:使用默认构造方法初始化HashMap。HashMap在一开始初始化的时候会返回一个空的table,并且thershold为0。因此第一次扩容的容量为默认值DEFAULT_INITIAL_CAPACITY也就是16。同时threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。

第二种:指定初始容量的构造方法初始化HashMap。那么从下面源码可以看到初始容量会等于threshold,接着threshold = 当前的容量(threshold) * DEFAULT_LOAD_FACTOR。

第三种:HashMap不是第一次扩容。如果HashMap已经扩容过的话,那么每次table的容量以及threshold量为原有的两倍。newCap = oldCap << 1

当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1),扩容都是按照2的幂次方扩容,因此newCap = 2^n

分析

resize方法中的oldTab首次初始化后table为null

判断当oldCap>0时表示扩容过 当前table容量 > 最大容量Capacity是返回当前table

当2倍的oldCap没有超过最大容量时并且oldCap大于等于默认初始容量16,table的容量就扩充两倍,threshold也就扩大了两倍。

接着分为使用带初始化容量的构造参数和使用默认的构造参数进行扩容

在默认的构造方法中

  1. newCap = DEFAULT_INITIAL_CAPACITY; 新的容量

  2. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 新的threshold

把每个桶中的数据移动到新的桶中,如果是红黑树按照红黑树的方式处理。

如果是链表把表分成两个链表,减少迁移量。判断是否需要移动链表。e.hash & oldCap成立则不需要移动反之移动。loTail不等于空时放入原索引hiTail不为空时原索引+oldCap放到桶里,也就是扩充为原索引+旧的容量。

扩展

HashMap初始化后首次插入数据时,先发生resize扩容再插入数据之后每当插入的数据个数达到threshold时就会发生==resize,==此时是先插入数据再resize

 类似资料: