HashMap底层源码分析


一、知识点

加载因子:
HashMap 的默认的加载因子: 0.75,用来限定阈值(用于控制 HashMap 的饱和度)
阈值 = 加载因子 * 数组长度
默认阈值: 16 * 0.75 = 12 (默认的初始容量:16)
在数组中存储元素数量,超过阈值,会引发数组扩容

Hash算法:
不是加密算法,不可逆
常见的哈希算法:MD4 、 MD5 、SHA1、SHA2、 SHA3

hashmap 的 hash值的计算:
// 不是哈希算法,仿照哈希算法思想
// 不够充分随机,但是速度快
// 要存储 key-value 数据,对 key 计算,得到一个 int 类型的 hash 值
// 如果kay值是null, 它对应的hash值是0
// 如果kay值不是null, 取key的hashCode 异或上  hashCode向right移动16位    
// hashCode 向低位移动16位, 让高位和低位取异或运算的目的, 就是希望高位和低位充分混合, 最终都能参加到取模预算中去 
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

手动计算HashMap的hash值举例:
String str = "study";
int hash = str.hashCode();
hash = hash ^ (hash >>> 16);
// 将哈希值映射到有限的索引范围内
int index = hash % 底层数组长度;

// hashCode(用于记录对象的位置)不一样, 那么hash值一定不一样, hashCode和hash值是一一对应关系
如果一个key-value数据在数组x下标位置, 当这个数组扩容, 扩容完成之后, 这个key-value数据要重新散列, 要么还在x位置, 要么在旧数组长度+x位置
因为默认的扩容机制是2

二、数据结构

class HashMap{
    Node[] table; // HashMap的底层数组
    int size;
    float loadFactor; // 加载因子
    float DEFAULT_LOAD_FACTOR = 0.75f;	// 默认加载因子
    int threshold; // 阈值
    // ......
}

// 存储到HashMap底层Node数组中结点的类型:
// 添加key-value数据,实际上存储到对应经过计算 hash 值取模得到的下标的位置
class Node<K,V>  {
	final int hash;		// 扩容数组后,原本存在某个位置的元素可能改变位置
	final K key;
	V value;
	Node<K,V> next;    	// HashMap底层数组中是链表
}

三、resize() 扩容方法

给定数组长度点和 ArrayDeque 中的 initialCapacity 类似,只是 initialCapacity 是大于,没有等于
在构造方法里给 HashMap 一个长度, 它会把这个长度变成一个大于等于这个值的最小的2的幂值作为底层数组的长度
HashMap 的底层结构一直会保持2的幂值

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;
	    }
	    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
	             oldCap >= DEFAULT_INITIAL_CAPACITY)
	        newThr = oldThr << 1; // 默认的扩容机制:2倍
	}
	else if (oldThr > 0) 
	    // 场景一:第一次添加数据扩容,阙值不为0,数组为0
	    // 比如调用构造方法 new HashMap(initCap,loadFactor); new HashMap(initCap); 等
	    newCap = oldThr;
	else { // 场景一:第一次添加数据扩容,oldTab = null,oldCap = 0,oldThr = 0  
	    // DEFAULT_INITIAL_CAPACITY = 1 << 4;
	    // newCap(新容量) =  16
	    newCap = DEFAULT_INITIAL_CAPACITY;
	    // newThr(新阈值) = 0.75 * 16 = 12
	    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
	}
	
	// 新阙值未赋值,如上面的if-else if不成立
	if (newThr == 0) {
	    float ft = (float)newCap * loadFactor;
	    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
	              (int)ft : Integer.MAX_VALUE);
	}
	
	threshold = newThr;
	Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	table = newTab;
	
	// 上面的代码是在创建新的数组
	// 这里把旧数组数据转移到新数组中
	if (oldTab != null) {...}
	
	return newTab;
}

四、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;

    // 扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    // (n - 1) & hash : hash值和数组长度取模
    // 因为数组长度是2的幂值,所以可以用&操作
    // i 取模之后的下标
    // p 指向key-value数据经过取模之后的下标位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K, V> e; // e指向需要修改的元素
        K k;
        // 要添加的key的位置,不为空,比较key
        // 先比较hash值,再看两个key是否直接相等或者相equals
        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 {
            // 这个下标位置存储的不是树,是链表
            // 初始:p指向数组的“第一个元素”
            for (int binCount = 0; ; ++binCount) {
                // 注意:这里 e = p.next,e指向链表的第二个元素(数组中的数组为第一个元素),此时 binCount == 0
                if (( e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);                                           
                    // 当链表长度超过8达到9个数据的时候会进行红黑树的转化(阈值是8)
                    // HashMap中出现红黑树的概率很低
                    if (binCount >= TREEIFY_THRESHOLD - 1) // >= 7
                        // 转化树,不一定会构造树(发生树的转化,但不一定会将链表转换成红黑树)
                        treeifyBin(tab, hash);
                    break;
                }
                // 链表中有重复元素, key相同,新值覆盖旧值
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                
                p = e;
            }
        }
        if (e != null) { 
            // 新值覆盖旧值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // ...
    return null;
}

转化树的源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
	int n, index; Node<K,V> e;
当底层数组数为空,或者底层数组的长度 < MIN_TREEIFY_CAPACITY (初始值为64) 时,会选择扩容,而不是创建树
因为底层数组的长度是2的幂次值,< 64,即底层数组的长度最大为 320.75 的加载因子,即底层数组允许存储的数据的最大值是 32*0.75 = 24,这时即使某一个下标位置已经产生长度为9的链表, 是选择扩容, 而非转化为红黑树resize();
	if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

注意:
如果我们使用某些特殊的类型来充当key, 要根据上面这个条件判断是否需要重写hashCode和equals方法

在这里插入图片描述


五、remove() 删除方法

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;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        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;
        
        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);
            }
        }
        
        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)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
    
// 注:也可使用map.clear();将map中的所有元素全部删光

六、removeTreeNode() 退化链表方法

情况一:
移除元素 remove() 操作:
当红黑树(根节点/根节点的左右结点/根节点的左结点的左结点)这四个结点有任意一个不存在, 就要由红黑树转化为链表  
在final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,boolean movable) {...}中
关键代码:            
if (root == null || root.right == null ||
    (rl = root.left) == null || rl.left == null) {
    tab[index] = first.untreeify(map);  // too small
    return;
}


情形二:
底层数组的扩容resize()方法,导致树拆成两部分,树中的结点过少,转化成链表
判断节点类型,如果是链表,则将链表拆分,如果是TreeNode,则执行TreeNode的split方法分割红黑树
if (lc <= UNTREEIFY_THRESHOLD) // 低位;UNTREEIFY_THRESHOLD = 6
	tab[index] = loHead.untreeify(map);
if (hc <= UNTREEIFY_THRESHOLD) // 高位
	tab[index + bit] = hiHead.untreeify(map);

为什么链表转化为红黑树阈值是8, 红黑树转化为链表阈值是6 ?
担心如果阈值设为一样, 添加和删除可能导致红黑树和链表间的反复转化

相关推荐

  1. HashMap底层面试题

    2024-06-14 17:30:03       15 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-14 17:30:03       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-14 17:30:03       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-14 17:30:03       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-14 17:30:03       20 阅读

热门阅读

  1. Windows Docker Desktop 安装 postgres

    2024-06-14 17:30:03       11 阅读
  2. aspose-words去水印自用资源

    2024-06-14 17:30:03       10 阅读
  3. k8s_用CronJob控制器实现定期任务举例

    2024-06-14 17:30:03       7 阅读
  4. 【Go语言】Go语言中的接口类型

    2024-06-14 17:30:03       8 阅读
  5. 【Flutter】路由组件的应用 (学习记录)

    2024-06-14 17:30:03       8 阅读
  6. Spring Boot中使用logback出现LOG_PATH_IS_UNDEFINED文件夹

    2024-06-14 17:30:03       7 阅读
  7. 数值类型==和equals别用错

    2024-06-14 17:30:03       6 阅读
  8. Qt QMake指南(如何写pro文件)

    2024-06-14 17:30:03       10 阅读
  9. Rabbit MQ和Kafka的区别

    2024-06-14 17:30:03       6 阅读
  10. Kafka基础架构与核心概念?有哪些应用场景?

    2024-06-14 17:30:03       6 阅读
  11. Kafka之ISR机制的理解

    2024-06-14 17:30:03       6 阅读
  12. 10.GLM

    10.GLM

    2024-06-14 17:30:03      7 阅读