深入浅出解析HashMap源码

尚硅谷-深入java8的集合3:HashMap的实现原理.pdf 本教程特点: 1.更适合零基础学员: ·自Java语言起源始,循序渐进,知识点剖析细致且每章配备大量随堂练习,让你步步为营,学得透彻、练得明白 ·拒绝晦涩难懂的呆板教学,宋老师语言生动幽默,举例形象生动深入浅出,迅速让你把握问题本质,四两拨千斤 2.课程内容推陈出新: ·基于JDK 11,将Java8、Java9、Java10、Java11新特性一网打尽 ·课程中,Eclipse和IDEA这两种企业一线开发环境都使用到了 3.技术讲解更深入、更全面: ·课程共30天,715个知识视频小节,涉及主流Java使用的方方面面,全而不冗余 ·全程内容涵盖数据结构、设计模式、JVM内存结构等深度技术 ·企业级笔试面试题目深入源码级讲解,拒绝死记硬背 4.代码量更大、案例更丰富、更贴近实战: ·Java语言基础阶段:12720行代码,Java语言高级阶段:11684行代码 ·课堂实战项目3套,课后实战项目2套 ·近百道企业面试真题精讲精练、极具实战性 立即下载

转载自:https://huanglei.rocks/coding/inside-jdk-hashmap.html (该个人博客十分geek)

基于 OpenJDK1.8

1 综述

1.1 内部类和字段

1.1.1 Node<K,V>

实现了Map.Entry接口,代表链表状态下 HashMap 里面存放的一个元素。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    //...
}

Node当中有一个next字段,指向另一个Node实例。也就是说Node是链表的一个元素。

1.1.2 TreeNode<K,V>

TreeNode<K,V>是Node<K,V>的一个子类,用于当链表升级为红黑树时存储 entry。

1.1.3 table:Node<K,V>[]

节点元素的数组,即所谓的 bucket

1.1.4 modCount:int

HashMap 被修改的次数。多线程条件下可能某个线程迭代 map 的时候另一个线程修改了 map 的元素,可能导致数据的不一致。而迭代的线程可以通过在迭代开始和结束的时候的modCount来判断是否有另一个线程在这个过程中修改了数据,如果修改了则抛出ConcurrentModifictionException(详见java.util.HashMap#forEach)。

1.2 底层数据结构

HashMap 会根据当前 table 的大小和冲突情况,逐渐升级存储的数据结构,数组->链表->RBT。

一开始 HashMap 由一个 Entry 数组支撑,也就是table:Map.Entry<K,V>[]。table当中的每一个元素都是某个链表的头结点,结构如下:
这里写图片描述

  • 如果两个 key 的 hashCode 不同,那么这两个元素被放在table:Node<K,V>[]数组的不同位置上
  • 如果两个 key 的 hashCode 相同,也就是发生了 hash 冲突,则两个元素被 append 到table:Node<K,V>[]的相同位置的链表尾部

显然,hashCode 的设计直接关系到 HashMap 的查询效率。如果 hashcode 没有冲突,那么 HashMap 的查询效率是O(1),如果出现了 hash 冲突,那么 HashMap 的查询效率下降到O(N)。

2 插入

2.1 流程综述

通过 key 的hashCode方法获取 hashCode,对 hashCode 调用HashMap.hash()方法使高位的 bits 分散到低位来,然后通过hash()的返回值决定当前的 entry 到底放在哪一个 bucket 当中(即table:Node<K,V>[]的哪一个位置上)。

  • 如果算出来的槽位上本来没有元素,那么直接把这个 entry 放在这个位置上即可
  • 如果槽位上面已经是一个红黑树节点,那么把用 entry 构造新的 rbt 节点插入到这棵树上
  • 如果槽位上面是一个普通链表节点,那么把当前 entry append 到链表尾部。判断链表是否达到需要升级的阈值,如果达到阈值则将链表转换为红黑树
  • 如果插入之后的 map 的容量已经达到扩容阈值(capacity*loadfactor),那么对 map 进行扩容(HashMap#resize())

2.2 hash()

HashMap#put()方法实际上是对HashMap#putVal()方法的调用。

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

首先通过HashMap.hash()方法来计算 hash 值(整数)。

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

hash()方法读取 key 的 hashCode,并且把高 16 位与第 16 位 XOR。

这里 XOR 主要是为了解决这种场景:如果某些 hashCode 只有高 16 位不同而低 16 位全部一致,那么这些 hashCode 永远都会冲突从而降低效率。典型的例子就是以一组连续近似相同的float为 key 的数据。比如下面这个例子:
2.123111112f: 0x4007E10D
3.123111113f: 0x4047E10D
4.123111114f: 0x4083F087
5.123111115f: 0x40A3F087
6.123111116f: 0x40C3F087
7.123111117f: 0x40E3F087
可见低 16 位冲突比较严重。移位+XOR 是速度、实现难度等的一种折中实现。

2.3 putVal()

putVal()是一个插入数据的多功能实现,执行具体的插入操作,具有很多可选参数,这里不进行解释。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; //bucket
    Node<K,V> p;    //待插入的 entry
    int n, i;       //
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//当前为空,则创建 table
    if ((p = tab[i = (n - 1) & hash]) == null)//bucket 没有元素占用(无冲突)
        tab[i] = newNode(hash, key, value, null);//直接占用当前 bucket
    else {//出现冲突,p 为 bucket 上面已有的元素
        Node<K,V> e; K k;
        //待插入的数据和原来的数据的 key 完全一致
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)//如果原来 bucket 里面就是 rbt 的根节点
            //那么构造 rbt 节点插入到 rbt 当中
            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);
                    //判断是否到升级为 rbt 的阈值
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);//将链表升级为 rbt
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;//增加修改计数(modification count)用于检测并发修改
    if (++size > threshold)//如果达到扩容阈值
        resize();//进行扩容
    afterNodeInsertion(evict);
    return null;
}

3 性能:容量和负载因子

3.1 容量 Capacity

Capacity 是table:Node<K,V>[]数组的长度。

HashMap 会根据存放的数据数量进行扩容,但是不管怎么扩容其容量都是 2^n。这个设计是为了计算通过 hashCode 计算 bucket 槽位的时候方便。
HashMap 是通过tab[i = (n - 1) & hash来计算当前 Entry 到底是放在table:Node<K,V>[]的哪一个 bucket 当中的,而老版本的 JDK 使用tab[i = hash % n,只有当 n 为 2 的整数次幂的时候,这两个计算((n - 1) & hash和hash%n)才能等价。使用按位与&取代取模%主要是因为&一般是单周期指令而%需要用到除法器,速度相差好几倍。

3.2 负载因子 LoadFactor

负载因子是 HashMap 元素总数与 capacity 的比值,用于衡量当前的 bucket table 装了多满。
高负载因子可以增大空间利用率,但是会降低查询和插入的效率;低负载因子浪费空间而查询插入效率高。
初始情况下负载因子为 0.75。
当 HashMap 的容量大于 Capacity*LoadFactor 的时候,就会对table:Node<K,V>[]进行扩容:

//OpenJDK1.8 HashMap.java:662
if (++size > threshold)//threshold 即为 capacity*loadfactor,每次 resize 的时候重新计算
    resize();

负载因子本身就是在控件和时间之间的折衷。当我使用较小的负载因子时,虽然降低了冲突的可能性,使得单个链表的长度减小了,加快了访问和更新的速度,但是它占用了更多的控件,使得数组中的大部分控件没有得到利用,元素分布比较稀疏,同时由于Map频繁的调整大小,可能会降低性能。但是如果负载因子过大,会使得元素分布比较紧凑,导致产生冲突的可能性加大,从而访问、更新速度较慢。所以我们一般推荐不更改负载因子的值,采用默认值0.75.

4 resize()的实现

由于table:Node<K,V>[]的长度永远都是 2 的整数次幂,因此 resize 之后的元素所在的槽位要么是在原地,要么是在移动 2^k 的位置上。
比如一开始容量为 16(2^4),也就是只有hash()(h^(h>>>16))运算之后的值的最低 4 位决定到底将 Entry 放在table:Node<K,V>[]数组的哪一个位置,即掩码为0x0000000F
扩容之后,容量变为16>>1=32,hash()后的值的低 5 位参与计算槽位,掩码为0x0000001F。

  • 如果某个 key 的 hash 之后的值为0xAABBCC0A,则扩容之前的槽位为0xAABBCC0A & 0x0000000F = 0x0000000A=10(dec),扩容之后的槽位为0xAABBCC0A & 0x0000001F = 0x0000000A=10(dec),可见对于这个 key,扩容前后没有变化;
  • 相反的,如果某个 key 的 hash 之后的值为0xAABBCCDA则扩容之前的槽位为0xAABBCCDA & 0x0000000F = 0x0000000A=10(dec),扩容之后的槽位为0xAABBCCDA & 0x0000001F = 0x0000001A = 26(dec),即原来的槽位偏移了 16。

这个设计的特点在于,扩容的时候,不需要重新计算槽位,只需要知道对于原来的table:Node<K,V>[]里面的每一个 Entry(Node),它的 hash 值(即hash()运算之后的值)按位与上扩容之后的 mask 上面新增的那一个 bit(这个 bit 用 16 进制表示出来正好就是旧的table的 capacity,比如 16 扩容到 32,mask 新增的那一个 bit 即是0x00000010(dec 16)),其值为 0 还是 1。如果按位与的值为 0,那么这个 entry 在新的table里的下标与原来相同;如果按位与的结果为 1,那么其在新table的下标等于原来的下标+扩容前的 capacity。

for each Node in oldTable:
    if Node.hash & oldCapacity == 0:
        newTable[Node 在 oldTable 当中的下标]= Node //保留在 newTable 的原位
    else:
        newTable[Node 在 oldTable 当中的下标 + oldCapacity] = Node

5 其他:entrySet

一个 HashMap 可以有两种视图,一种是 map 视图,也就是键值对;另一种是 set 视图,即把 hashmap 看做若干个Entry<K,V>元素组成的 set。HashMap的entrySet成员变量就是用来缓存当前 hashmap 的 set 视图的。entrySet并不存储任何数据,它只是返回一个迭代器(java.util.HashMap.EntrySet#iterator) 或者对 HashMap 的每一个元素执行某个Consumer<? super Map.Entry<K,V>>操作而已(java.util.HashMap.EntrySet#forEach)

HashMap源码分析 立即下载

相关推荐

HashMap源码深度剖析.md

HashMap源码深度剖析,面试必备

threshold函数_HashMap初始化时的Capacity、loadFactor与threshold

从hashMap的构造函数开始:initialCapacity:初始化容量,默认16loadFactor:装载因子,默认0.75构造函数的最后以initialCapacity做入参调用了tableSizeFor方法,来看下这个方法的作用:返回给定目标容量的二次幂,也就是返回比入参大的最小的2的n次幂。例如:传入8,返回8;传入9,返回16.其中,|=为或等运算符,>>>...

weixin_39586353的博客 797

HashMap源码(上)

HashMap的部分源码解析

HashMap中capacity、loadFactor、threshold、size等概念的解释

约定 约定前面的数组结构的每一个格格称为桶约定桶后面存放的每一个数据称为binbin这个术语来自于JDK 1.8的HashMap注释。 size size表示HashMap中存放KV的数量(为链表和树中的KV的总和)。 capacity capacity译为容量。capacity就是指HashMap中桶的数量。默认值为16。一般第一次扩容时会扩容到64,之后好像是2倍。总之...

diaoju3333的博客 285

手把手教你用Zemax优化MTF:从‘离焦平衡球差’到高频像差控制的实战技巧

本文详细介绍了如何使用Zemax优化MTF性能,从基础设置到高级技巧的全流程。通过具体案例,讲解了如何利用离焦平衡球差提升中频MTF,以及识别和控制高频像差的方法,帮助光学设计工程师和学生有效提升成像质量。

weixin_30752699的博客 748

基于Python爬虫的新闻热点分析与挖掘

本文介绍了基于Python爬虫的新闻热点分析与挖掘方法,主要包括数据收集、预处理、热点分析和可视化四个部分。首先通过爬虫抓取新浪新闻数据,利用文本清洗和存储技术进行数据预处理;接着采用TF-IDF和LDA算法进行关键词提取与主题建模;最后通过词云图和主题分布图实现数据可视化。该方法能有效识别新闻热点,为信息筛选和决策提供支持。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 1544

java HashMap 源码分析(深度讲解)

java 集合篇章——HashMap源码分析(非常详细)。

优快云 / 稀土掘金/ B站 / GitHub: Cyan_RA9 2559

ESD放电路径分析

当金属外壳接地情况下,大部分外部干扰信号将会通过外壳,外壳到接地点,再由接地点回到干扰信号的地,实现干扰信号的回流,这点不仅仅适用于​ESD,对于其他的RI类干扰,也同样适合。发现内部金属板跟外壳的搭接处存在较多的缝隙点,该缝隙点对于频率较高的信号能够实现较为理想的回流,但是对于频率没有那么高的信号,则较难以实现回流,我们尝试将这些缝隙用铜箔包裹住,充分降低该金属板跟外壳的接触阻抗​。我们分析,路径3其实是不期望的路径,我们最期望的路径是1,或者2的内部金属部件跟外壳良好接触情况下的路径。

domen_pan的博客 2226

JAVA并发编程:深入解析JDK1.8中HashMap、并发容器ConcurrentHashMap原理及其源码分析

一、HashMap详解 1、基本介绍 1.1 概述

菜鸟技术栈的专栏 1517

激光可燃气体传感器:核心技术环节分析

激光可燃气体传感器核心技术涵盖七个维度:1)TDLAS光谱吸收与调制技术(检测下限ppm级);2)DFB激光器与双闭环温控系统(波长稳定性±0.0036nm);3)微弱信号处理技术(信噪比提升60dB);4)环境补偿算法(温压补偿误差<±1%);5)本质安全防爆设计(ExiaIICT4Ga等级);6)智能边缘计算(STM32L476芯片);7)多重性能验证(响应时间<1秒)。通过微型化、多气体检测等创新,该技术已在工业安全等领域替代传统方案,实现高精度检测。(149字)

上市公司前技术负责人,20 年软硬件深耕 + 百人研发团队管理经验,名校理工科背景加持。从技术攻坚到团队掌舵,沉淀海量项目实战与管理心得,专注分享软硬件干货、疑难问题解决方案,既是靠谱知识传播者,也盼链接优质项目共成长。 1039

最新Linux CSI Tools安装教程(两台主机,一发一收)(监控模式)

无线感知之CSI Tools工具安装,最新在监控模式下采集CSI数据,

h1998040218的博客 3029

深入浅出HashMap

  java.util.HashMap深度学习 一、散列表初探:       同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。一个算法的评价主要从时间复杂度和空间复杂度来考虑。        时间复杂度是指执行算法所需要的计算工作量;而空间复杂度是指执行这个算法所需要的内存空间。两者均体现在消耗计算机重要的两个资源...

JavaFresh 342

Windows环境下DataEase一站式安装指南(含WSL2+Docker配置)

本文详细介绍了在Windows环境下通过WSL2和Docker快速部署DataEase的完整教程。从WSL2配置、Docker环境搭建到DataEase离线安装,逐步指导用户完成一站式部署,特别适合需要快速搭建数据分析平台的开发者。教程包含常见问题解决方案和性能优化技巧,帮助用户高效使用这款国产开源数据可视化工具。

weixin_42526166的博客 392

HashMap的原理

HashMap的原理 注意:此博客搬运别人,为的是加深自己记忆,若有冒犯请包含 文章目录HashMap的原理1. 数组的优势2. 数组不够,我们还需要键值对3. Hash函数4. Hash冲突5. 再看HashMap源码 **本文主要包含以下内容,力求深入浅出一步一步彻底明白HashMap的设计思想:** 数组的优势 数据是特殊的键值对 Hash函数 Hash冲突 此时再看HashMap源码 1. 数组的优势 上图是一个含有8个元素的整型数组,数组下标从0到7,如果我们要获取第四个元素的值,直接a[3

菜鸟的专栏 259

加速访问Cloudflare的网站的解决方案

本文主要介绍了加速访问Cloudflare的网站的解决方案,希望能对大家有所帮助。通过软件计算得到最快的IP地址的效果图如下图所示,具体解决方案参考下文。 在这里插入图片描述 文章目录 1. 问题描述 2. 解决方案

weixin_43178406的博客 1万+

Claude Code 从入门到精通,通过实战项目穿插 Agent Skills、MCP、Hooks、子代理、插件等十五个知识点

本文介绍了Claude Code的高级使用技巧,通过一个个人简历网站实战项目,详细讲解了15个核心知识点。主要内容包括:三种权限模式(默认/编辑/计划)的使用与切换;历史会话管理(/resume、/rename、/clear命令);会话回滚功能(/rewind);以及危险参数--dangerously-skip-permissions的注意事项。文章还演示了如何通过计划模式拆分代码文件,并强调了在复杂任务中仔细检查规划步骤的重要性。这些技巧能帮助开发者更高效地使用Claude Code完成项目开发。

1788

一篇文章彻底读懂HashMap之HashMap源码解析(上)

就身边同学的经历来看,HashMap是求职面试中名副其实的“明星”,基本上每一加公司的面试多多少少都有问到HashMap的底层实现原理、源码等相关问题。 在秋招面试准备过程中,博主阅读过很多关于HashMap源码分析的文章,漫长的拼凑式阅读之后,博主没有看到过一篇能够通俗易懂、透彻讲解HashMap源码的文章(可能是博主没有找到)。秋招结束后,国庆假期抽空写下了这篇文章...

小胖的博客 458

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

仿写HashMap源码

主要是通过get和put方法来理解Hashmap来理解HashMap如何存键值对,没有涉及太深,红黑树请绕行 先贴出手写的HashMap的demo 一、定义Map接口 public interface Map<K,V> { /** * 向Map中插入值 */ public V put(K k,V v); /** * 根据...

MONKEY_Quan的博客 256

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

2024年最新深入浅出解析阿里成熟的微前端框架 qiankun 源码【图文并茂】,2024年最新hashmap数据结构面试

四轮技术面+一轮hr面结束,学习到了不少,面试也是一个学习检测自己的过程,面试前大概复习了 一周的时间,把以前的代码看了一下,字节跳动比较注重算法,面试前刷了下leetcode和剑指offer, 也刷了些在牛客网上的面经。大概就说这些了,写代码去了~祝大家都能收获大厂offer~开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】篇幅有限,仅展示部分内容qiankun注意:子应用沙箱中的proxy对象(第 62 行)可以简单理解为子应用的window。

2401_84617821的博客 724

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

深入浅出Java集合

深入浅出Java集合 1.集合框架概述 集合、数组都是对多个数据进行存储操作的结构,简称Java容器。 说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中) 数组在存储多个数据方面的特点: 一旦初始化以后,其长度就确定了。 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。 比如:String[] arr; int[] arr1; Object[] arr2; 数组在存储多个数据方面的缺点: 一旦

xingranzhihuo的博客 336

【Java-集合】HashMap-Hash冲突解决

【Java-集合】HashMap-Hash冲突解决 2017年10月22日 20:49:58binke_mxb阅读数:5371 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.youkuaiyun.com/u012712901/article/details/78313130 背景:我们常用HashMap作为我们Java开发时的K-V数据存储结构(如id-perso...

evilcry2012的专栏 872

史上最详细HashMap讲解,详细到行,这还不会,你趁早转行

史上最详细HashMap讲解,详细到行,这还不会,你趁早转行 申明一下,不是我写的,但我看懂了,太详细了!必须推荐:地址:https://blog.youkuaiyun.com/leisure_life/article/details/108482840​​​​​​​ ...

weixin_38087046的博客 142

互联网大厂Java面试实录:严肃面试官VS搞笑水货程序员谢飞机的三回合较量

好的,今天的面试就到这里。你的基础概念有一定了解,但在深度和细节上还需要加强。请回去等我们的通知。

weixin_38599038的博客 646

「集合底层」深入浅出HashMap底层源码

HashMap底层采用了哈希表,而哈希表是由数组和链表实现的。数组和链表各有自己的特点: 数组:占用空间连续。 寻址容易,查询速度快。但是,增加和删除效率非常低。 链表:占用空间不连续。 寻址困难,查询速度慢。但是,增加和删除效率非常高。 所以HashMap既有数组的优点也有链表的优点(即查询快,增删效率也高)。

frozenpenguin的博客 1790

源码解析 - HashMap 浅析

本文是源码入门浅析,突破我们对源码的恐惧。 希望大家不要看到源码就畏惧,一点点来,我们一样可以一步步成为大神。 前面赠送一个简单的面试题给大家 HashMap和Hashtable的区别: HashMap 是从1.2开始的;Hashtable是从1.0开始的 HashMap 是线程不安全的;Hashtable 是线程安全的 HashMap 可以存 null 值 null 键;hashtable ...

FresherHe的博客 327

HashMap 源码深入浅出

为什么一个支持多线程一个不支持,看代码就知道。 为什么一个可以放空,一个不可以,看代码就知道。没有为什么,设计如此。 https://baijiahao.baidu.com/s?id=1618550070727689060&wfr=spider&for=pc ...

男儿不展风云志,空有天生八尺躯 413
上一篇: 傻傻分不清:Spring IoC注入,自动装配与循环依赖
下一篇: 实践出真知:理解Spring Bean生命周期
猪杂汤饭
博客等级 码龄12年 238粉丝 67原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值