hashmap value可以为空吗_HashMap关键点分析

本文详细分析了HashMap的核心概念,包括数据结构、关键参数、主要方法和流程。HashMap允许key和value为null,但key只能有一个null。JDK1.8中HashMap采用数组+链表+红黑树,当链表长度大于8时转为红黑树。扩容策略是每次扩容为2的倍数。此外,文章还探讨了HashMap的添加、查找、删除等操作的关键细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


基于JDK版本:1.8

一、核心概念
  1. 数据结构:从jdk1.8之后,HashMap采用数组+链表+红黑树(Red-Black Tree)。因为这个特性,HashMap的查询时间复杂度理论上为O(1),但实际上是:O(1)+O(n)+O(logn),因为链表长度大于(TREEIFY_THRESHOLD = 8 )会自动转成红黑树,所以当数据Key的哈希值冲突时,时间复杂度将会达到O(logn)。

  2. key和value可以为null,但key只能存在一个null,位于组数的0位置:因为hash(key)方法,当key=null ,返回0。存储对象时,存储为key的对象需要重写equals()方法。

  3. 线程不安全,多线程高并发情况下,容易形成 循环链表。高并发场景可以使用Collections.synchronizedMap(Map map) 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。

  4. HashMap初始容量为16,超过:扩容阈值(threshold) = 哈希表容量 * 加载因子(load factor) 进行扩容, 每次扩容2的倍数。且保证容量为2^n个数

  5. HashMap JDK1.8 中扩容后,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap。在 1.7的时候扩容后,链表的节点顺序会倒置,1.8则不会出现这种情况。

  6. hash方法的区别:HashMap 会对 key 的 hashCode 返回值做进一步扰动函数处理。

  • 1.7 中扰动函数使用了 4次位运算 + 5次异或运算;

  • 1.8 中降低到 1次位运算 + 1次异或运算;;

    扰动处理后的 hash 与 哈希表数组length -1 做位与运算得到最终元素储存的哈希桶角标位置。

HashMap数据结构

3d7c511d5b2158a944de3acb1ef3913d.png

二、关键参数
//默认初始化容量,保证为2的倍数。
static final int DEFAULT_INITIAL_CAPACITY = 1 <4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 <30;
//默认加载因子:如果因子太小,则会频繁扩容,因子太大,则键冲突多,转成链表或树。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//触发转成树时链表(桶)的临界阈值,大于2,小于8.
static final int TREEIFY_THRESHOLD = 8;
//转成链表时树的个数临界阈值
static final int UNTREEIFY_THRESHOLD = 6;
//转成树时数组的个数,至少为4*TREEIFY_THRESHOLD,才能避免扩容与转换成树冲突。
//在treeifyBin()方法内有判断
static final int MIN_TREEIFY_CAPACITY = 64;

//加载因子,如果未指定使用的是,DEFAULT_LOAD_FACTOR——无参构造方法
final float loadFactor;
//库容阈值 = 容量 * 加载因子
int threshold;
//存储哈希桶的数组,哈希桶中装的是一个单链表或一颗红黑树,长度一定是 2^n
transient Node[] table;//所有键值对的Set集合 区分于 table 可以调用 entrySet()得到该集合transient Set> entrySet;//HashMap中存储的键值对的数量注意这里是键值对的个数而不是数组的长度(table.length)transient int size;////操作数记录 为了多线程操作时 Fast-fail 机制transient int modCount;
三、主要方法
  1. 构造方法

    主要决定两个参数:loadFactor和threshold。

    其中threshold通过方法:tableSizeFor(initialCapacity)得到

    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最近的且是2的倍数 ,比如:cap=11,返回16(2^4)。


  1. hash方法

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    原理:把 key 的 hashCode 方法返回值右移16位,即丢弃低16位,高16位全为0 ,然后在于 hashCode 返回值做异或运算,即高 16 位与低 16 位进行异或运算,这么做可以在数组 table 的 length 比较小的时候,也能保证 高低Bit都参与到 hash 的计算 中,同时不会有太大的开销,扰动处理次数也从 4次位运算 + 5次异或运算 降低到 1次位运算 + 1次异或运算。


  1. 添加方法 putVal

    这个方法属于向HashMap添加元素的基础方法,关联方法:put/putMapEntries/putIfAbsent。

    该方法分三个核心点:

  • 空,在数组直接添加
  • 红黑树:把需要添加的数据转成红黑树。
  • 链表:如果链表元素超过:TREEIFY_THRESHOLD(8) 转成树。
  • 链表:遍历元素,为空则添加,存在key一致,如果可以覆盖oldValue,则覆盖后返回oldValue。
  • 通过index =  (n - 1) & hash,获取HashMap数组对应的节点
  • 对当前节点判断:
  • 如果超过了扩容的阈值,进行扩容resize()。
//hash:key的hash值(使用方法hash(key))
//onlyIfAbsent:如果true,不替换原值,方法putIfAbsent()传true
//evict:如果是false代表HashMap正在初始化
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {}
  1. 定义:Node[] tab; Node p; int n, i;
  2. [if] HashMap数组为空或者长度为0
  1. 进行HashMap的初始化resize(),并获取数组的长度:table.length
  1. [if] 通过index = (n - 1) & hash,定位到数据元素为空。——未发生hash碰撞。
  1. 直接在当前index生成一个Node:newNode(hash, key, value, null)
  1. [else] 代表发生hash碰撞了。进一步判断(先进行key判断,后进行value判断):
  1. 定义:Node e; K k。
  2. [if] 当前索引对应的Node,key的hash值和key本身与要添加的都相同。
  1. 复制一份当前Node,后续进行value判断。
  1. [else if] 如果当前索引的Node属于TreeNode(红黑树)
  1. 通过方法:((TreeNode)p).putTreeVal(this, tab, hash, key, value),添加红黑树节点。
  1. [else] 链表情况:循环遍历,中断条件为内部判断逻辑。
  1. [if] 节点Node的next(e = p.next)元素为空:最后一个
  1. 生成一个新的Node,并赋予p.next
  2. 如果循环遍历次数超过 TREEIFY_THRESHOLD 8个,则则进行树的转换:treeifyBin(tab, hash)。
  3. 终断循环。
  1. [if] key的hash值和key和key本身与要添加的都相同。
  1. 终断循环。
  1. 循环到最后,获取最后一个Node——p = e
  1. [if] 存在key对应的节点映射Node,即key冲突。
  1. [if] onlyIfAbsent = true 获取原有value为空
  1. 覆盖原有value。
  1. 进行预留后置处理:afterNodeAccess(e);——LinkedHashMap使用
  2. [return] 原有value(oldValue)——即如果放入的key相同,则返回被覆盖的value。
  1. 增加操作次数:++modCount
  2. [if] (++size > threshold) 超过了扩容的阈值
  1. 进行扩容:resize()。
  1. 进行预留后置处理:afterNodeInsertion(evict);——LinkedHashMap使用(重写)。
  2. [return] null

  1. 列表扩容方法 resize

该方法整体分为三部分:

  • 初始化扩容
  • 寻找扩容后数组的大小以及新的扩容阈值
  • 非初始化时将旧的table复制到新的table中

关键核心点:

JDK1.8 中扩容后,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap。

final Node[] resize() {}
  1. 复制一份当前HashMap的table:Node[] oldTab = table;
  2. 确定oldTab的容量和扩容阈值,定义newTab的容量和阈值为0。
  3. [if] 旧容量大于0,实际上旧扩容阈值也大于0。
  1. [if] 旧容量大于最大值:MAXIMUM_CAPACITY
  1. 把扩容阈值threshold设置为最大值:Integer.MAX_VALUE
  2. 直接返回原来表。不进行扩容
  1. [else if] 旧容量X2后,小于最大值:MAXIMUM_CAPACITY 且旧容量大于等于 DEFAULT_INITIAL_CAPACITY
  1. 新容量newThr = oldThr << 1, 扩容两倍
  1. [else if] 旧扩容阈值大于0,即旧容量等于0:代表正在初始化
  1. 新扩容阈值 = 旧扩容阈值
  1. [else] 这个情况代表 未初始化
  1. 设置newCap = DEFAULT_INITIAL_CAPACITY
  2. newThr =DEFAULT_LOAD_FACTOR X DEFAULT_INITIAL_CAPACITY
  1. [if] 新扩容阈值 newThr == 0:只有一种情况:loadFactor人为设置很高
  1. 计算新容量的扩容阈值。
  2. 如果新容量扩容阈值大于最大值 MAXIMUM_CAPACITY 则设置newThr = Integer.MAX_VALUE,否则设置为实际计算的。
  1. 当前threshold = newThr。
  2. 进行新旧表复制 :新建表newTab = (Node[])new Node[newCap];
  3. 当前table = newTab
  4. [if] oldTable != null——空表不需要复制
  1. 循环遍历旧表容量值
  2. [if] 循环的当前节点不为空,赋予新Node e = oldTab[index]
  1. 旧表当前值(oldTab[index])置为空。——释放旧表空间
  2. [if] e节点没有next节点。——即当前节点为数组节点
  1. 复制旧表数据到新表索引(e.hash & (newCap - 1))位置。——位置一致。
  1. [else if] 旧表当前节点是红黑树
  1. 使用方法:((TreeNode)e).split(this, newTab, j, oldCap),进一步确认一步确定树中节点位于新数组中的位置。
  1. [else] 链表结构
  1. 定义链表的高位头尾(loHead/loTail)和低位节点(hiHead/hiTail)
  2. 循环do-while,直到next为空。——遍历旧链表。
  1. 定义next = e.next

  2. [if] (e.hash & oldCap) == 0判断,属于低位区域,在低位的头尾赋值e。

  3. [else] 属于高位区域,在高位的头尾赋值e。

  1. 如果低位尾不为空:loTail != null
  1. 设置loTail.next = null
  2. 将低位链表存放在 原index(j) 处。
  1. 如果高位尾部不为空:hiTail != null
  1. hiTail.next = null;
  2. 将高位链表存放在 新index(j + oldCap) 处。
  1. 返回新表newTab

  1. 添加map实例 putMapEntries

    批量元素添加,主要用于:putAll()/HashMap(Map extends K, ? extends V> m)构造方法/clone()复制方法

    核心流程:

  • 判断是否初始化
  • 判断是否需要扩容
  • 遍历待添加map,调用putVal接口一一添加。
 final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {}
  1. 获取入参Map的size:int s = m.size();
  2. 只有入参Map的size>0,才继续操作。
  3. [if] 当前map 未初始化:table = null。
  1. 待扩容size/loadFactor +1,与最大值判断,取得预估容量t。
  2. 如果ft大于现有hashMap的threshold,则使用方法tableSizeFor(t)得到最接近t且为2^n的容量。
  1. [else if] 待添加的hashMap容量大于扩容阈值:threshold。
  1. 进行扩容:resize()。
  1. 遍历待添加的hashMap。
  1. 获取对应的key和value。
  2. 使用方法: putVal(hash(key), key, value, false, evict),进行添加。

  1. 查询节点 getNode

    根据key的hash值获取对应的Node,主要用于get(Object key)/containsKey(Object key)/replace(K key, V oldValue, V newValue) 等方法。

    这些方法最终获取的是Node.value。

    核心流程:

  • 通过经过hash后的key,以获取数组的形式获取Node节点:tab[(n - 1) & hash]
  • 如果Node不为空,判断key值是否一样
  • 如果对应Node的next不为空,是否属于TreeNode,通过getTreeNode获取对应Node。
  • 如果是链表,循环获取对应的Node,判断key是否一致,返回对应Node。
final Node getNode(int hash, Object key) {}
  1. [if] 当前hashMap,table !=null 且 size>0 且tab[(n - 1) & hash]) != null,数组中有对应的Node。
  1. [if] hash值和key值相等。
  1. 直接返回对应的Node
  1. [if] (e = first.next) != null,即Node位置为链表或者TreeNode。
  1. [if] Node节点属于TreeNode
  1. 返回通过方法.getTreeNode(hash, key),获取到的Node——从TreeNode的父级节点开始find节点。
  1. [else] 链表结构,循环遍历链表,直到next == null。
  1. 返回hash和key值一样的Node节点。
  1. 返回null;数组为空,不需要删除。

  1. 删除节点 removeNode

    这个方法有多个入参,决定value是否一致才删除,或者删除后TreeNode是否在删除后“平衡移动”

    核心流程:

    final Node removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {}
  • 通过key找到对应的Node
  • 如果找得到(value是否一致)删除对应的Node
  • 进行其他移动(TreeNode),—size
  1. 当前hashMap,table !=null 且 size>0 且tab[(n - 1) & hash]) != null,数组中有对应的Node。同getNode的方法
  1. [if] hash一致且key一致,说明是数组节点。
  1. 提取对应的Node,后续使用 :node = p
  1. [else if] 当前Node有next节点,说明是链表或红黑树:
  1. [if] Node节点属于TreeNode
  1. 通过方法.getTreeNode(hash, key),获取到的Node——从TreeNode的父级节点开始find节点。
  2. 提取对应的Node,后续使用 :node。
  1. [else] 链表结构,循环遍历链表,直到next == null。
  1. 得到hash和key值一样的Node节点。
  1. 提取对应的Node,后续使用 :node = e
  1. 循环遍历到最后一个元素
  1. 提取对应的Node,后续使用 :p = e确认匹配不到对应的节点!
  1. [if] node != null,如果需要严格按照value值是否一致,则进行匹配。条件成立说明能找到要删除的节点:
  1. [if] node属于TreeNode
  1. 通过方法:removeTreeNode(this, tab, movable)删除对应的node节点,并根据参数:movable 确认是否移动Tree节点。
  1. [else if] node == p, 属于数组节点:
  1. 直接把当前索引Node指定为下一个:tab[index] = node.next。
  1. [else] 属于链表:
  1. 直接把当前节点的next指定为定位到的节点的next,链表缩减  p.next = node.next。
  1. ++modCount, --size
  2. 进行预留后置处理:afterNodeRemoval(node);——LinkedHashMap使用(重写)。
  1. 返回null;数组为空,不需要删除。

  1. 删除节点 clear

    删除HashMap的所有节点。源码比较简单。

    核心流程:

  • 确认表不为空,为空不处理。
  • 把size置为零。
  • 循环数组:table[index] = null。暴力方式,Node如果是Tree或者链表无引用后被回收(为何不单独判断红黑树或者链表?)
四、流程图
HashMap的put方法:

a2cfb5548810e5502e4aab8fd1557c21.png


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值