HashMap 的设计与优化//Java、后端、自主学习、经验交流

面试题库(二)1:hashmap1.7跟1.8?优化点?红黑树化为什么是8?退化为什么? 当元素的键值对过于集中时,也会导致大量的元素存储在同一个桶中,从而降低 HashMap 的性能。当哈希函数的冲突率过高时,会导致大量的元素存储在同一个桶中,从而降低 HashMap 的性能。HashMap 1.8 在 1.7 的基础上进行了许多优化,提高了性能和稳定性。 阅读详情

hashmap 是一个 key-value 形式的键值对集合。(本文内容基于 JDK1.8)下面是一个简单的 hashmap 的结构。 本文主要是通过源码的方式分析 HashMap 的实现和优化。主要是围绕源码本身展开,以添加注释的方式进行记录和分析

初始化

在创建 HashMap 对象示例的时候不会初始化存储数组,会在首次调用 put 方法的时候初始化数组。构造方法如下:

public HashMap(int initialCapacity, float loadFactor) {
    // initialCapacity 初始容量 < 0 报错; 默认 16
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // initialCapacity 初始容量是否大于最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // loadFactor 负载因子 <= 0 || isNaN ; 默认0.75
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

put 方法

添加数据通常我们采用 put 方法,该方法也是我们开发中比较常用的方法之一。最终会调用 putVal 方法进行初始化和添加数据。在这个方法中我们需要注意的有几个地方:

  1. 如果没有初始化会调用 resize() 方法进行 hashmap 存储数组的初始化。
  2. 默认通过 & 运算计算节点存储位置,这里也证明了为什么初始化数组的长度要是 2 的 n 次方。
  3. 如果不存在 hash 冲突的情况下,通过然后调用 newNode 方法创建节点,存放到对应的数组下标。
  4. 如果存在 hsah 冲突的情况下。这里就会有三种情况:
  • 首次 hash 冲突的情况下,当前节点是一个普通的节点,如果 key 相同得话,将采取数据覆盖的方式;
  • 如果当前节点类型是 treeNode 类型,是一棵红黑树。将调用 putTreeVal 方法来进行添加子节点;
  • 最后,将当作链表处理,首先查找链表的尾节点,找到尾节点后,将当前节点添加到尾节点,这里有一个判断如果当前链表的节点数 > 8 并且 hashmap 的总长度 > 64 就会将当前的链表进行变换为红黑树。还有一种特殊情况,如果在链表的查找过程中查找到了一个当前新增key 相同的节点,那么就会覆盖当前节点数据并且退出循环;
  1. 前面所有的步骤都是为了找到当前的节点指针,然后再通过当前对象修改 value 值, 这里有一个需要注意的地方,在修改的时候会做一个判断如果 **_onlyIfAbsent_** 等于 false 才可以修改,就是说可能当前 hashmap 存在不可以被修改的情况,比如:map.putIfAbsent 方法的时候调用就会修改失败,最后才能修改 value 值,并且返回旧值。
  2. 最后对修改次数累加,然后判断一次是否需要拓展 hashmap 的容量,然后返回 null , 方法结束。
// put 方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// putVal 方法
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)
        // 调用 resize 初始化
        // n = tab.length 容量
        n = (tab = resize()).length;
    // 默认通过 & 运算计算节点存储位置,这里也证明了为什么初始化数组的长度要是2的n 次方
    // 并且把当前节点的数据给 p
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果节点数据已经存在,即存在 hash 冲突的情况
        Node<K,V> e; K k;
        // 1. 当前节点存在,并且插入k,和存在的 k 的value 值相同,那么直接刷新当前节点数据即可
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 新的节点数据 = p, 其实这里也只是获取 p 的指针
            e = p;
        // 2. 如果是 TreeNode 结构, 即红黑树结构
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 3. 其他情况,即链表结构
            for (int binCount = 0; ; ++binCount) {
                // 父节点子节点为 null
                if ((e = p.next) == null) {
                    // 将 p.next = newNode
                    p.next = newNode(hash, key, value, null);
                    // 节点数是否大于变形的阈值 (TREEIFY_THRESHOLD = 8)
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 如果 tab.length < 64 默认拓容
                        // 否则进行红黑树转换
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果存在值相同的情况
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果 e 不为空,就是说当前节点指针不为空,【这种情况是覆盖】,返回旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 当前节点可以被修改或者是新增节点
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 预留模板方法
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 修改次数 ++
    ++modCount;
    // 大于拓容阈值
    if (++size > threshold)
        // 拓容
        resize();
    // 预留模板方法
    afterNodeInsertion(evict);
    return null;
}

总结:其实通过上面的分析和代码的,我们分析出有一下几个核心方法:

  • newNode 创建 Node 节点
  • ((TreeNode<K,V>)p).putTreeVal(**this**, tab, hash, key, value);添加节点信息;
  • treeifyBin 节点冲突情况下,链表转换为红黑树;
  • resize() HashMap 拓容;

newNode 创建节点

创建 HashMap 的节点信息,其实这个方法看上去比较普通,但是本质上也是比较普通。但是对于 hash 这个参数我们可以思考一下。

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

hash 计算 hash 码

hash 方法如下,

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

理论上 hash 散列是一个 int 值,如果直接拿出来作为下标访问 hashmap 的话,考虑到二进制 32 位,取值范围在**-2147483648 ~ 2147483647** 范围。 大概有 40 亿个 key , 只要哈希函数映射比较均匀松散,一般很难出现碰撞。 存在一个问题是 40 亿长度的数组,内存是不能放下的。因为咱们 HashMap 的默认长度为 16 。所以这个 hashCode , (key.hashCode ) 是不能直接来使用的。使用之前先做对数组长度的与运算,得到的值才能用来访问数组下标。 代码如下:

// n = hashmap 的长度
p = tab[i = (n - 1) & hash]) 

这里为什么要使用 n -1 ,来进行与运算,这里详单与是一个”低位掩码”, 以默认长度 16 为例子。 和某个数进行与预算,结果的大小是 < 16 的。如下所示:

    10000000 00100000 00001001
&   00000000 00000000 00001111
------------------------------
    00000000 00000000 00001001  // 高位全部归 0, 只保留后四位 

这个时候会有一个问题,如果本身的散列值分布松散,只要是取后面几位的话,碰撞也会非常严重。还有如果散列本省做得不好的话,分布上成等差数列的漏洞,可能出现最后几位出现规律性的重复。 这个时候“扰动函数”的价值制就体现出来了。如下所示:

在 hash 函数中有这样的一段代码: (h = key.hashCode()) ^ (h >>> 16) 右位移 16 位, 正好是32bit 的一半,与自己的高半区做成异或,就是为了**混合原始的哈希码的高位和低位,以此来加大低位的随机性。**并且混合后的低位掺杂了高位的部分特征,这样高位的信息变相保存下来。其实按照开发经验来说绝大多数情况使用的时候 hashmap 的长度不会超过 1000,所以提升低位的随机性可以提升可以减少hash 冲突,提升程序性能。 ​

Node.putTreeVal

当前是一棵红黑树那么就需要添加节点

final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

treeifyBin 链表树化

如果 hashmap 的长度小于 64 会优先选择拓容,否则会当前冲突 key 所在的结构由链表转换为红黑树。 这个是 jdk 1.8 才有的新特征,hashmap 在 hash 冲突后可能由链表变化为红黑树结构。这样做的目的是为了提高读写效率。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 不一定树化还可能是拓容,需要看数组的长度是否小于 64 MIN_TREEIFY_CAPACITY
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // hd 头节点, tl 尾节点
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 将链表转换为树结构
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            // 转换红黑树操作,这里循环比较,染色、旋转等
            hd.treeify(tab);
    }
}

replacementTreeNode 方法

replacementTreeNode 方法主要是将 Node 转换为 TreeNode

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

TreeNode#treeify 方法

treeify 方法主要是将树结构转换为红黑树。

final void treeify(Node<K,V>[] tab) {
    // 根节点默认为 null
    TreeNode<K,V> root = null;
    // 链表遍历,x 指向当前节点,next 指向下一个节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        // 下一个节点
        next = (TreeNode<K,V>)x.next;
        // 设置当前节点的 left, right 为 null
        x.left = x.right = null;
        // 如果没有根节点
        if (root == null) {
            // 当前父节点为 null
            x.parent = null;
            // 当前红色节点属性设置为 false (把当前节点设置为黑色)
            x.red = false;
            // 根节点指向当前节点
            root = x;
        }
        // 如果已经存在根节点
        else {
            // 获取当前链表的 key
            K k = x.key;
            // 获取当前节点的 hash
            int h = x.hash;
            // 定义当前 key 所属类型
            Class<?> kc = null;
            // 从根节点开始遍历,此遍历设置边界,只能从内部跳出
            for (TreeNode<K,V> p = root;;) {
                // dir 标识方向(左右)ph 标识当前节点的 hash 值
                int dir, ph;
                // 当前节点的 key
                K pk = p.key;
                // 如果当前节点 hash 值大于当前 链表节点的 hash 值
                if ((ph = p.hash) > h)
                    // 标识当前节链表节点会放在当前红黑树节点的左侧
                    dir = -1;
                else if (ph < h)
                    // 右侧
                    dir = 1;
                // 如果两个节点的 key 的 hash 值相等,那么通过其他方式进行比较
                // 如果当前链表节点的 key 实现了comparable 接口,并且当前树节点和链表节点是相同的 class 实例,那么通过 comparable 比较
                // 如果还是相等再通过 tieBreakOrder 比较一次
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p; // 保存当前树节点
                // 如果 dir 小于等于 0: 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左子节点,
                // 也可能是左子节点或者右子节点或者更深层次的节点
                // 如果dir 大于等于 0:  当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右子节点,
                // 也可能是右子节点或者左子节点或者更深层次的节点
                // 如果当前树节点不是叶子,那么最终以当前树节点的左子节点或者右子节点为起始几点,然后再重新开始寻找自己当前链表节点的位置。
                // 如果当前树节点就是叶子节点,那么更具 dir 的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
                // 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp; // 当前链表节点作为当前树节点的子节点
                    if (dir <= 0)
                        xp.left = x; // 左子节点
                    else
                        xp.right = x; // 右子节点
                    root = balanceInsertion(root, x); // 重新平衡
                    break;
                }
            }
        }
    }
    // 把所有的链表节点都遍历之后,最终构造出来的树可能是经历多个平衡操作,根节点目前到底是链表的那个节点是不确定的
    // 因为我们需要基于树来做查找,所以就应该把 tab[N] 得到的对象一定是根节点对象,而且是链表的第一个节点对象,所以要做对应的调整。
    // 把红黑树的节点设置为所在数组槽的第一个元素
    // 说明: TreeNode 既是一个红黑树也是一个双向链表
    // 这个方法做的事情是保证树根节点一定要成为链表的首节点
    moveRootToFront(tab, root);
}

balanceInsertion 树平衡

这个方法分析之前,我们可以先看看红黑树的规则:红黑树是每个结点都带有颜色属性的二叉查找树,颜色或红色或黑色。 在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 性质1. 节点是红色或黑色。
  • 性质2. 根节点是黑色。
  • 性质3. 所有叶子都是黑色。(叶子是NIL结点)
  • 性质4. 每个红色节点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  • 性质5. 从任一节节点其每个叶子的所有路径都包含相同数目的黑色节点。
// root 为根节点
// x 为需要插入的节点
// 最后返回的是一个平很后的根节点
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    // 查询节点标记为红色
    x.red = true;
    // 设置一个只可以内部退出的循环
    // 变量说明: 
    // xp 当前节点, xpp 父节点的父节点,  xppl 父节点的父节点的左节点, xppr 父节点的父节点的右节点
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        // 如果父节点为空, 说明当前节点就是根节点,那么直接把当前接待你标记为黑色返回当前节点。
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 如果当前节点为黑设色并且当前父节点为 null, 或者
        // 父节点为红色,但是 xpp 节点为空
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 当前节点等于 xppl
        if (xp == (xppl = xpp.left)) {
            //xppr != null 并且 是红色
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp; // 当前节点等于 xpp, 进入下一次循环
            }
            else {
                if (x == xp.right) { // 当前节点是父节点的右子节点
                    root = rotateLeft(root, x = xp); //父节点左旋
                    xpp = (xp = x.parent) == null ? null : xp.parent; // 获取 xpp
                }
                if (xp != null) { // 父节点不为空
                    xp.red = false; // 父节点设置为黑色
                    if (xpp != null) { // xpp 不为空
                        xpp.red = true; // xpp 为红色
                        root = rotateRight(root, xpp); // xpp 右旋转
                    }
                }
            }
        }
        else { // 如果 xp 是 xpp 的右节点
            if (xppl != null && xppl.red) { // xppl 不为空,并且为红色
                xppl.red = false; // xppl 设置为黑色
                xp.red = false; // 父节点为黑色
                xpp.red = true; // xpp 为红色
                x = xpp;  // x = xpp 进入下次循环
            }
            else {
                if (x == xp.left) { // 当前节点为父节点的左子节点
                    root = rotateRight(root, x = xp); // 根节点你右旋转
                    xpp = (xp = x.parent) == null ? null : xp.parent; // xpp = xp.parent
                }
                if (xp != null) { // xp != null
                    xp.red = false; // xp 为黑色
                    if (xpp != null) { // xpp != null 
                        xpp.red = true; // xpp 为红色
                        root = rotateLeft(root, xpp); // 左旋
                    }
                }
            }
        }
    }
}


// 节点左旋转
// root 当前根节点
// p 指定选装的节点
// 返回旋转后的根接待你(平衡涉及左旋右旋根根节点改变,所以需要返回最新的根节点)
// 示意图
//        pp                    pp
//        |                     |  
//        p         --->        r
//       / \                   / \
//      l   r                 p  rr 
//         / \               / \
//        rl  rr            l  rl
// 旋转做了几件事情?
// 1. 将 rl 设置为 p 的子接待你,将 rl 设置为父节点 p 
// 2. 将 r 的父节点设置 pp, 将 pp 的左子节点设或者右子接待你设置为 r 
// 3. 将 r 的左子节点设置为 p, 将 p 的父节点设置为 r
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                      TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    // 左旋的节点以及需要左旋的节点的右节点不为空
    if (p != null && (r = p.right) != null) { 
        // 要左旋转的右子节点 = rl , 
        if ((rl = p.right = r.left) != null) 
            // 设置 rl 父亲节点设置为 p
            rl.parent = p; 
        // 将 r 的父节点设置为 p 的父节点,如果 pp == null 
        if ((pp = r.parent = p.parent) == null) 
            // 染黑
            (root = r).red = false; 
        else if (pp.left == p) // 判断父节点是在 pp 的左边还是右边
            pp.left = r; // 如果是左子节点,把 pp.letf = r
        else
            pp.right = r; // 如果是右子节点, pp.reight = r
        r.left = p; // 最后将 r的左子节点设置为 p
        p.parent = r; // 最后将 p.parent 设置为 r
    }
    return root;
}

// 节点右旋转
// 右旋同理
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                       TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) {
        if ((lr = p.left = l.right) != null)
            lr.parent = p;
        if ((pp = l.parent = p.parent) == null)
            (root = l).red = false;
        else if (pp.right == p)
            pp.right = l;
        else
            pp.left = l;
        l.right = p;
        p.parent = l;
    }
    return root;
}

moveRootToFront 方法

把所有的链表节点都遍历之后,最终构造出来的树可能是经历多个平衡操作,根节点目前到底是链表的那个节点是不确定的。 因为我们需要基于树来做查找,所以就应该把 tab[N] 得到的对象一定是根节点对象,而且是链表的第一个节点对象,所以要做对应的调整。 把红黑树的节点设置为所在数组槽的第一个元素,这个方法做的事情是保证树根节点一定要成为链表的首节点。

static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    // root 节点不为空, 并且表不为空, 并且数组长度大于 0
    if (root != null && tab != null && (n = tab.length) > 0) {
        // 当前 Node 所在槽位
        int index = (n - 1) & root.hash;
        // 获取当前槽所在接待你
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        // 如果当前槽位节点不是首节点
        if (root != first) {
            // 后驱节点
            Node<K,V> rn;
            // 修改为首节点
            tab[index] = root;
            // rp 前驱节点为 root 的前驱节点
            TreeNode<K,V> rp = root.prev;
            // 后驱节点不为空
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null) 
                // 原来的头节点前驱节点指向新的头节点 root 节点
                first.prev = root;
            // root 节点的后驱节点指向之前的头节点
            root.next = first;
            // root 由于是头节点所以前驱节点为 null
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}

remove 方法

remove 方法的本质是将 key 值所在的节点的值设置为 nu

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

removeNode 方法

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;
    // tab 不为空, 数组长度大于 0, 当前节点数据不为 null
    // 不得不说 hashmap 源码的逻辑还是非常严谨的
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        // node 用来存储当前节点信息
        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;
}

removeTreeNode 方法

removeTreeNode 是删除节点的核心方法,删除的时候如果是一个普通节点就可以直接情况,如果是链表的话需要将当前节点删除。如果是红黑树的话,需要删除 TreeNode , 然后进行一次树平衡,或者将树转换为链表。

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        return;
    // 获取索引值
    int index = (n - 1) & hash;
    // 获取头节点,即树的根节点
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    // 当前节点的后驱节点,当前节点的前驱节点保存
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    // 前驱节点为 null 
    if (pred == null)
        // 当前是头节点,删除之后,头节点直接指向了删除节点的后继节点
        tab[index] = first = succ;
    else
        pred.next = succ;
    if (succ != null)
        succ.prev = pred;
    // 如果头节点(即根节点)为空,说明当前节点删除后,红黑树为空,直接返回
    if (first == null)
        return;
    // 如果头接单不为空,直接调用 root() 方法获取根节点
    if (root.parent != null)
        root = root.root();
    if (root == null
        || (movable
            && (root.right == null
                || (rl = root.left) == null
                || rl.left == null))) {
        // 链表化,英文前面的链表节点完成删除操作,故这里直接返回,即可完成节点删除
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    
    // p 当前节点; pl 当前节点左节点,pr 当前节点右节点
    // replacement p 节点删除后替代的节点
    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
    if (pl != null && pr != null) {
        // p 节点删除后, 他的左右节点不为空时, 遍历他的右节点上的左子树
        // (以下操作先让 p 节点和 s 节点交换位置,然后再找到 replacement 节点替换他 )
        TreeNode<K,V> s = pr, sl;
        while ((sl = s.left) != null) // find successor
            s = sl;
        // 通过上述操作 s 节点是大于 p 节点的最小节点(替换它的节点)
        // 将 s 节点和 p 节点的颜色交换
        boolean c = s.red; s.red = p.red; p.red = c; // swap colors
        // sr  s 节点的右节点
        TreeNode<K,V> sr = s.right;
        // pp  p 节点的父节点
        TreeNode<K,V> pp = p.parent;
        // 如果 pr 就是 s 节点
        if (s == pr) { // p was s's direct parent
            // 节点交换
            p.parent = s;
            s.right = p;
        }
        else {
            // 获取 s 的父节点
            TreeNode<K,V> sp = s.parent;
            // 将 p 节点的父节点指向 sp, 且 sp 节点存在
            if ((p.parent = sp) != null) {
                // 判断 s 节点的 sp 节点在左侧还是右侧, 将 p 节点存放在 s 节点一侧
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            // 将 pr 节点编程 s 节点的右节点,并且 pr 节点存在
            if ((s.right = pr) != null)
                // 将 s 节点编程 pr 节点的父节点
                pr.parent = s;
        }
        // 因为 s 节点的性质, s 节点没有左节点
        // 当 p 节点和 s 节点交换了位置,所以将 p 节点的左几点指向空
        p.left = null;
        
        // 将 sr 节点编程 p 节点的左节点,并且 sr 节点存在
        if ((p.right = sr) != null)
            // 将 p 节点编程 sr 的父节点
            sr.parent = p;
        // 将 pl 节点编程 s 节点的左节点,并且存在 pl 节点
        if ((s.left = pl) != null)
            // 将 pl 父节点赋值为s
            pl.parent = s;
        // s 父节点设置为 pp 并且 pp 节点存在
        if ((s.parent = pp) == null)
            // root 节点为 s
            root = s;
        // p 节点等于 pp.left 
        else if (p == pp.left)
            // pp 的左节点为 s
            pp.left = s;
        else
            // p 节点等于 pp.right 
            // pp 右节点为 s
            pp.right = s;
        
        // sr 不为空
        if (sr != null)
            // 替换节点为 sr
            replacement = sr;
        else
            // 否则替换节点为 p 
            replacement = p;
    }
    else if (pl != null)
        // 如果 pl 节点存在, pr 节点不存在,不用交换位置, pl 节点为替换为 replacement 节点
        replacement = pl;
    else if (pr != null)
        // 如果 pr 节点存在,  pl 节点不存在, 不用交换位置, pr 节点为替换为 replacement 节点
        replacement = pr;
    else
        // 如果都不存在 p 节点成为 replacement 节点
        replacement = p;
    
    // 以下判断根据上述逻辑查看,仅以p 节点的当前位置为性质, 对 replacement 节点进行操作
    if (replacement != p) {
        // 如果 replacement 不是 p 节点
        // 将 p 节点的父节点 pp 变成 replacement 节点的父节点
        TreeNode<K,V> pp = replacement.parent = p.parent;
        // 如果 pp 节点不存在
        if (pp == null)
            // replacement 变成根节点
            root = replacement;
        else if (p == pp.left)
            // 如果 pp 节点存在,根据 p 节点在 pp 节点的位置,设置 replacement 节点的位置
            pp.left = replacement;
        else
            pp.right = replacement;
        
        // 将 p 节点所有的引用关系设置为 null
        p.left = p.right = p.parent = null;
    }

    // 如果 p 节点是红色,删除后不影响  root 节点,如果是黑色,找到平衡后的根节点,并且用 r 表示
    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

    // 如果 p 是 replacement 节点
    if (replacement == p) {  // detach
        // 得到 pp
        TreeNode<K,V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            // pp 存在
            // 根据 p 节点的位置,将 pp 节点的对应为位置设置为空
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    // 移动新的节点到数组上
    if (movable)
        moveRootToFront(tab, r);
}

balanceDeletion 方法

删除节点后的树平衡方法 。

static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
                                           TreeNode<K,V> x) {
    // x 当前需要删除的节点
    // xp x 父节点
    // xpl x 父节点的左子节点
    // xpr x 父节点的右子节点
    for (TreeNode<K,V> xp, xpl, xpr;;) {
        if (x == null || x == root)
            // x 为空或者 x 为根节点
            return root;
        else if ((xp = x.parent) == null) {
            // 当 xp 为空,说明 x 为根节点,将 x 设置为黑色并且返回 x 节点。
            x.red = false;
            return x;
        }
        else if (x.red) {
            // x节点是红色,无需调整
            x.red = false;
            return root;
        }
        else if ((xpl = xp.left) == x) {
            // 如果x节点为xpl节点
            if ((xpr = xp.right) != null && xpr.red) {
                // 如果xpr节点不为空,且xpr节点是红色的
                // 将xpr设置为黑色,xp设置为红色
                xpr.red = false;
                xp.red = true;
                // 左旋
                root = rotateLeft(root, xp);
                // 重新将xp节点指向x节点的父节点,并将xpr节点指向xp的右节点
                xpr = (xp = x.parent) == null ? null : xp.right;
            }
            if (xpr == null)
                // 若xpr节点不存在
                // 则将x节点指向xp节点向上调整                
                x = xp;
            else {
                // sl xpr节点的左节点
                // sr xpr节点的右节点                
                TreeNode<K,V> sl = xpr.left, sr = xpr.right;
                if ((sr == null || !sr.red) &&
                    (sl == null || !sl.red)) {
                    // 若sr节点为空或者sr节点是黑色的,且sl节点为空或者sl节点是黑色的
                    // 将xpr节点变成红色
                    xpr.red = true;
                    // 则将x节点指向xp节点向上调整
                    x = xp;
                }
                else {
                    //sr和sl中存在一个红节点
                    if (sr == null || !sr.red) {
                        //此处说明sl是红节点,将sl节点设置为黑色
                        if (sl != null)
                            sl.red = false;
                        //将xpr节点设置为红色
                        xpr.red = true;
                        //右旋
                        root = rotateRight(root, xpr);
                        //将xpr节点重新指向xp节点的右节点
                        xpr = (xp = x.parent) == null ?
                            null : xp.right;
                    }
                    if (xpr != null) {
                        //如果xpr节点不为空,让xpr节点与xp节点同色
                        xpr.red = (xp == null) ? false : xp.red;
                        //当sr节点不为空,变成黑色
                        if ((sr = xpr.right) != null)
                            sr.red = false;
                    }
                    //存在xp节点

                    if (xp != null) {
                        //将xp节点设置为黑色
                        xp.red = false;
                        //进行左旋
                        root = rotateLeft(root, xp);

                    }
                    //将x节点指向root进行下一次循环时跳出
                    x = root;
                }
            }
        }
        else { // symmetric
            //当x节点是右节点
            if (xpl != null && xpl.red) {
                //当xpl节点存在且为红色
                //将xpl变为黑色,xp变为红色
                xpl.red = false;
                xp.red = true;
                //右旋
                root = rotateRight(root, xp);
                //将xpl节点重新指向xp节点的左节点
                xpl = (xp = x.parent) == null ? null : xp.left;
            }
            if (xpl == null)
                //如果xpl节点不存在,则xp节点没有子节点了
                //将x节点指向xp节点向上调整
                x = xp;
            else {
                //sl xpl节点的左节点
                //sr xpl节点的右节点
                TreeNode<K,V> sl = xpl.left, sr = xpl.right;
                if ((sl == null || !sl.red) &&
                    (sr == null || !sr.red)) {
                    //若sr节点为空或者sr节点是黑色的,且sl节点为空或者sl节点是黑色的
                    //将xpl节点变成红色
                    xpl.red = true;
                    //则将x节点指向xp节点向上调整
                    x = xp;
                }
                else {
                    //sr和sl中存在一个红节点
                    if (sl == null || !sl.red) {
                        //此处说明sr是红节点,将sr节点设置为黑色
                        if (sr != null)
                            sr.red = false;
                        //将xpr节点设置为红色
                        xpl.red = true;
                        //左旋
                        root = rotateLeft(root, xpl);
                        //将xpl节点重新指向xp节点的左节点
                        xpl = (xp = x.parent) == null ?
                            null : xp.left;
                    }
                    //如果xpl节点存在
                    if (xpl != null) {
                        //使xpl节点与xp节点同色
                        xpl.red = (xp == null) ? false : xp.red;
                        //如果sl节点存在
                        if ((sl = xpl.left) != null)
                            //将sl节点变为黑色

                            sl.red = false;
                    }
                    // 如果xp节点存在
                    if (xp != null) {
                        // 将xp节点设置为黑色
                        xp.red = false;
                        // 右旋
                        root = rotateRight(root, xp);
                    }
                    // 将x节点指向root进行下一次循环时跳出
                    x = root;
                }
            }
        }
    }
}

线程安全

HashMap 不是线程安全的集合, 如果要使用线程安全的 k-v 集合可以使用 CurrentHashMap.

注意事项

使用 Map 的方法 keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。 ​

集合初始化时,指定集合初始值大小 解释: HashMap 使用 HashMap(int initialCapacity) 初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可。 initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值) 举例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容, resize()方法总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。 ​

使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。 values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。 ​

Map 类集合 K/V 能不能存储 null 值的情况,如下表格:

**集合类 **KeyValueSuper说明
hashtable不允许为 null不允许为 nullDictionary线程安全
ConcurrentHashMap不允许为 null不允许为 nullAbstractMap锁分段技术(JDK8: CAS)
TreeMap不允许为 null允许为 nullAbstractMap线程不安全
HashMap允许为 null允许为 nullAbstractMap线程不安全

由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储null 值时会抛出 NPE 异常


加我微信,备注999,送你超值大礼包(记得备注,不然可能会忘记送你资源了)

或者加QQ群我们一起交流:731892759
免费领取价值3000元的Java资料
1、Java自学超详细电子书
2、自录优质网络直播课
3、大厂Java面试题,优质项目练习题

 

 

 

Java 集合框架:HashMap 的介绍、使用、原理与源码解析 HashMap 根据是一个键值对集合,采用 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。HashMap 最多只允许一条记录的键为 null。HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。 阅读详情

相关推荐

SRC漏洞挖掘实战指南:从零入门到月入过万的网络安全副业

网络安全的核心在于识别和修复系统缺陷,其中逻辑漏洞与信息泄露是常见的安全风险。其原理在于应用程序在业务逻辑设计或数据处理过程中存在缺陷,导致未授权访问或敏感数据暴露。掌握这些技术对于构建安全的应用体系至关重要,不仅能有效防范攻击,也为安全研究人员提供了将技术能力变现的合法途径。在漏洞赏金(Bug Bounty)和SRC(安全应急响应中心)等应用场景中,白帽子通过系统化的测试方法,挖掘此类漏洞,既能获得经济回报,又能积累宝贵的实战经验。本文聚焦于逻辑漏洞与信息泄露的实战挖掘技巧,为初学者提供清晰的入门路径和效

weixin_28568819的博客 346

高性能场景下,HashMap的优化使用建议

1. HashMap 在JDK 7 与 JDK8 下的差别 顺便理一下HashMap.get(Object key)的几个关键步骤,作为后面讨论的基础。 1.1 获取key的HashCode并二次加工 因为对原Key的hashCode质量没信心,怕会存在大量冲突,HashMap进行了二次加工。 JDK7的做法:   h ^= (h >>> 20) ^ (h >>

liuyinlong 1714

【电商风控】电商风控标注是什么

本篇文章给大家谈谈。

866

深入分析hashmap

一、传统 HashMap的缺点 (1)JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。 (2)当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。 (3)针对这种情

lianhuazy167的专栏 2万+

OpenClaw记忆插件升级实战:从memory-lancedb-pro安装到AgentSkill封装

向量数据库作为现代AI应用的核心基础设施,通过将非结构化数据转换为高维向量并利用相似度检索实现语义理解。其原理基于嵌入模型生成向量表示,结合近似最近邻搜索算法实现高效查询。这项技术为智能体系统提供了长期记忆和上下文感知能力,显著提升了对话连贯性与任务执行效率。在AI智能体开发领域,向量数据库常用于知识库构建、对话历史管理和个性化推荐等场景。本文聚焦OpenClaw框架中memory-lancedb-pro插件的部署实践,针对**插件加载顺序**和**Apache Arrow依赖**两大常见痛点,通过封装成可

weixin_42525482的博客 637

运算放大器 VS 仪表放大器

运算放大器(Operational Amplifier, 简称 Op Amp)和仪表放大器(Instrumentation Amplifier, 简称INA)在电子电路设计中都有非常广泛的应用,但两者在结构、特性以及应用场景上都有明显的区别。以下是它们之间主要的差异:运算放大器(Op Amp)仪表放大器(INA)运算放大器仪表放大器运算放大器仪表放大器运算放大器仪表放大器运算放大器仪表放大器总之,运算放大器是通用型放大元件,需要设计者自己做额外的电路配置;而仪表放大器则是针对高精度测量差分信号而生,内部匹配

chinahongjiang的博客 1346

HashMap源码解读:探究Java 8中的变化与优化

HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长.HashMap是非线程安全的,只适用于单线程环境,多线程环境可以采用并发包下的concurrentHashMapHashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆。

资料免费分享,点击名片 318

银河飞腾FT6678实战指南:OpenMP多核并行编程优化技巧

本文详细介绍了在银河飞腾FT6678多核处理器上使用OpenMP进行并行编程的实战技巧与优化方法。从基础语法到环境配置,再到性能优化的五大核心技巧,包括循环并行化、并行粒度控制、reduction子句使用等,帮助开发者充分发挥FT6678的8核计算能力。通过实际案例展示如何将图像处理算法从200ms优化到28ms,并深入探讨了任务调度、内存访问优化等进阶主题。

weixin_29234423的博客 285

HashMap源码逐行分析

1.数据结构 1.Node static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = va

lhf931003的博客 315

【100%通过率】华为OD机试真题 Java&Go 实现【查找单入口空闲区域】【2022.11 Q4 新题】

给定一个 m xn 的矩阵,由若干字符 和0构成,X表示该处已被占据,0"表示该处空闲,请找到最大的单入口空闲区域.解释:空闲区域是由连通的O组成的区域,位于边界的0可以构成入口,单入口空闲区域即有目只有一个位于边界的0作为入口的由连通的'O"组成的区域。 如果两个元素在水平或垂直方向相邻,则称它们是“连通”的。

MISAYAONE的博客 2万+

基于STM32L4XX 、HAL库的ATD5984步进电机驱动器驱动程序应用设计

一、简介: ATD5984是一款内部集成了译码器的微特步进电机 驱动器,能使双极步进电机以全、半、1/4、/1/8、1/16 和1/32步进模式工作。步进模式由逻辑输入管脚MSx选 择。其输出驱动能力达到28V±1.6A。 译码器是ATD5984易于使用的关键,通过STEP管脚 输入一个脉冲就可以使电机完成一次步进,省去了相序 表,高频控制线及复杂的编程接口,使其更适于没有复 杂的微处理器或微处理器负担过重的场合。 在步进操作期间,ATD5984的内部电路可以自动

PP_Cottage的博客 404

Java集合之一—HashMap

深入浅出学Java——HashMap 哈希表(hash table) 也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中的对应实现HashMap的实现原理进行讲解,然后会对JDK7的HashMap源码进行分析。 一、什么是哈希表 在讨论哈希表之前,我们先大概了解下其他数据结构在新增,...

深入浅出学JAVA 60万+

【即插即用模块】注意力篇 | TGRS 2025 | CDFIM:通道+空间多尺度注意力,双模态特征融合增强,性能翻倍!

C2DFF-Net提出了一种轻量级跨模态特征融合模块CDFIM,通过模态差异感知和多尺度深度滤波增强双模态互补信息。该模块采用通道-空间注意力协同机制,以极低计算成本(仅增加0.26M参数)提升多模态目标检测性能,在三个遥感数据集上平均提升mAP约1.7%。适用于无人机等边缘设备的实时检测任务,为跨模态融合提供高效解决方案。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 623

深入浅出HashMap的设计与优化

HashMap通过哈希表数据结构的形式来存储键值对,这种设计的好处就是查询键值对的效率高。我们在使用HashMap时,可以结合自己的场景来设置初始容量和加载因子两个参数。当查询操作较为频繁时,我们可以适当地减少加载因子;如果对内存利用率要求比较高,我可以适当的增加加载因子。我们还可以在预知存储数据量的情况下,提前设置初始容量(初始容量=预知数据量/加载因子)。这样做的好处是可以减少resize()操作,提高HashMap的效率。

echohuangshihuxue的博客 754

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

《java性能调优》3.HashMap的设计与优化

HashMap 作为我们日常使用最频繁的容器之一,相信你一定不陌生了。今天我们就从 HashMap 的底层实现讲起,深度了解下它的设计与优化。 常用的数据结构 ArrayList 是基于数组的数据结构实现的,LinkedList 是基于链表的数据结构实现的,而我今天要讲的 HashMap 是基于哈希表的数据结构实现的。我们不妨一起来温习下常用的数据结构,这样也有助于你更好地理解后面地内容。 数组:...

回不到昨天@ 2788

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

07 - 深入浅出HashMap的设计与优化

HashMap 通过哈希表数据结构的形式来存储键值对,这种设计的好处就是查询键值对的效率高。我们在使用 HashMap 时,可以结合自己的场景来设置初始容量和加载因子两个参数。当查询操作较为频繁时,我们可以适当地减少加载因子;如果对内存利用率要求比较高,我可以适当的增加加载因子。我们还可以在预知存储数据量的情况下,提前设置初始容量(初始容量 = 预知数据量 / 加载因子)。这样做的好处是可以减少 resize() 操作,提高 HashMap 的效率。

。 3157

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Java 后端开发(从菜鸟到起飞)

岗位,年薪范围 30W-80W(一线城市)。:集合框架(源码级理解 HashMap、ConcurrentHashMap)、IO/NIO、多线程(JUC 包):Lambda 表达式、Stream API、模块化(Java 9+)、Record 类(Java 16+):数据结构与应用场景(HyperLogLog、Geo)、持久化策略、集群模式(Cluster):内存模型(堆/栈/方法区)、类加载机制、GC 算法(CMS/G1/ZGC):B+ 树索引、事务隔离级别(MVCC 实现)、锁机制(行锁/间隙锁)

m0_60036990的博客 5272

深入解析Java HashMap的高性能扩容机制与树化优化

通过treeifyBin方法和resize方法的源码分析,可以看出Java 8中HashMap在处理哈希冲突和扩容方面的优化手段:树化:当桶中链表长度超过阈值时,将链表转换为红黑树,以提高查询效率。●扩容优先:如果哈希表容量不足,则优先进行扩容,而不是树化,以避免在小容量时引入树结构。扩容处理树节点:在扩容过程中,保留红黑树结构,并根据新链表长度决定是否退化为链表。这些机制共同保证了HashMap在处理大量数据时的高效性。理解这些实现细节有助于在实际使用中优化HashMap的性能。

微笑听雨 9363

【Java】HashMap的详细介绍

扩容过程中,节点会从旧数组迁移到新数组,头插法在迁移时会反转链表顺序(例如旧链表 A→B,迁移后新链表变为 B→A)。若此时有多个线程同时操作,可能出现节点引用相互指向的情况(如 A.next = B 且 B.next = A),形成环形链表。后续查询时,线程会陷入无限循环,导致 CPU 占用飙升。原因是:数组容量小时,扩容成本低,通过扩容可分散元素,减少冲突;基于泊松分布,当负载因子为 0.75 时,链表长度自然增长到 8 的概率极低(约千万分之一),此时说明哈希冲突异常频繁,需要转为红黑树优化。

chaodddddd的博客 1777

性能篇,Hashmap的设计与优化?

嘿,小伙伴们!今天我们要深入研究程序员最钟爱的数据结构之一——HashMap。从实现结构到添加、获取元素再到扩容,我们将一一揭开HashMap性能优化的面纱。不容错过的技术分享,快来和小米一起探索吧

孤芳不自赏 969

Java性能优化(三):Java基础-HashMap的设计与优化

HashMap核心特性数据结构:HashMap采用哈希表数据结构来存储键值对,利用哈希函数和哈希表快速定位元素位置,提供高效的键值对查询。参数设置初始容量:HashMap允许用户根据使用场景设定初始容量,以优化性能。在预知数据量时,可以通过计算(初始容量=预知数据量/加载因子)来设定合适的初始容量,以减少扩容操作,提高效率。加载因子:加载因子定义了哈希表何时进行扩容的阈值。加载因子较小时,哈希表会更早地进行扩容,减少哈希冲突;加载因子较大时,会提高内存利用率但可能增加哈希冲突。

1603

Java 后端开发技术学习总结:实用代码示例与项目实操经验大公开

通过对 Java 后端开发技术从基础语法到框架应用,再到数据库交互和项目实战的深入学习,我们逐步构建起坚实的知识体系。然而,技术的发展日新月异,Java 生态也在不断演进。未来,我们需要持续关注新的框架版本、性能优化技巧以及云计算、微服务等新兴领域与 Java 后端的融合。希望这篇学习总结能成为各位读者在 Java 后端之旅中的得力伙伴,让我们一起在代码的世界里不断探索,创造出更强大、更高效的后端应用。

2301_80350265学无止尽5的博客 6685

从幕后揭秘:HashMap 与 ConcurrentHashMap 的全面演进与 JDK8 优化

本文将系统回顾 Java 标准库中两大哈希表实现——HashMap 与 ConcurrentHashMap——从 JDK 1.2 到 JDK17 的演化历程,结合 Java 内存模型原理,深入剖析其在不同版本下的底层设计以及算法优化;并通过汇编级别分析、性能对比、生产案例和生态对比,全面呈现哈希表在高并发、大数据量场景中的实践与调优;最后展望容器在 Valhalla、Project Loom 等未来特性中的前景。地点HashMap 吞吐 (ops/s)

lssffy的博客 1802

【Java数据结构】HashMap 的深入解析与优化实践

《JDK8 HashMap实现原理与优化分析》摘要: HashMap是Java核心数据结构,JDK8引入"数组+链表+红黑树"的复合结构提升性能。哈希算法通过扰动函数(hash>>>16)分散键值分布,采用位运算计算索引。冲突处理使用链地址法,链表长度超过8且容量≥64时转为红黑树(查询效率O(logn))。优化策略包括:扩容时避免重新计算哈希(利用高位差),树化阈值动态调整等。多线程环境下存在并发修改问题,建议使用ConcurrentHashMap替代。理解这些机制对

手心朝下的博客 1356

Java性能优化| 深入浅出HashMap的设计与优化

在上一讲中我提到过 Collection 接口,那么在 Java 容器类中,除了这个接口之外,还定义了一个很重要的 Map 接口,主要用来存储键值对数据。HashMap ...

sinat_27143551的博客 932

2019后端java相关学习计划

开篇介绍 笔者介绍:18年7月初入职一家业务高速发展的互联网教育公司,专职java后端开发。 从入职到现在,似乎一直沉浸在项目的开发上,还没什么时间好好沉淀下,一直都在做项目,每个月的工作基本就是需求评审-开发-测试-上线 ,是时候需要好好沉淀下来,梳理一下方向。 从java基础到微服务架构路线学习知识点 我理想中的职业发展路线: java基础 JDK源码 java8编程 深入理解java虚...

anguser 4759
上一篇: Java学习路线图//Java、Java学习路线、Java自学、Java经验分享、经验分享、资源分享
下一篇: 涨薪60%!从小厂逆袭,坐上美团L8技术专家(附面经+心得)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值