大揭秘:HashMap原理解析

本文涉及的产品
云解析DNS-重点域名监控,免费拨测 20万次(价值200元)
简介: 大揭秘:HashMap原理解析


🍊 底层工作原理

当我们向HashMap中插入元素(k1, v1)时,它会应用哈希算法获得一个哈希值,并将其映射到相应的内存地址。通过这种方式,我们可以获取与键相关的数据。如果该位置没有其他元素,它会直接放入一个Node类型的数组中。默认情况下,HashMap初始大小为16,并且负载因子为0.75。负载因子是一个介于0和1之间的浮点数,它决定了HashMap在扩容之前内部数组的填充度。因此,当元素数量达到12时,底层将进行扩容,将数组大小扩大为原来的2倍。如果该位置已经存在其他元素(k2, v2),那么HashMap将调用k1的equals方法和k2进行比较。如果返回值为true,则表明两个元素相同并使用v1替换v2。如果返回值为false,则表明两个元素不同,并以链表的形式存储(k1, v1)。不过,当链表中的元素数量较多时,查询效率将下降。为了解决这个问题,在JDK1.8版本中对HashMap进行了升级。具体而言,当HashMap中存储的数据满足链表长度超过8、数组长度大于64时,将会将链表替换为红黑树,以提高查询效率。

你可以把HashMap想象成一本电话簿,每个电话簿里都有很多联系人的名字和对应电话号码。但是,电话簿有个特殊的地方,它不是按照联系人名字的首字母顺序排列的,而是按照一个特别的规则排列的。

这个规则就是Hash算法。Hash算法的作用就是将联系人的名字通过一定的计算,转化成一个数字(hash值),然后再根据这个数字找到对应的页面,在页面中查找联系人的电话号码。就像我们在电话簿中找到联系人的电话号码一样。

但是,如果很多联系人的名字都是相似的,就像张三、张三三、张三四等,这时候用Hash算法就会遇到一个问题:如果两个联系人的名字经过Hash算法计算后得到的数字一样,就会出现“冲突”,也就是两个联系人的名字被映射到同一个页面上了。这时候,HashMap就需要解决这个冲突的问题。

HashMap的解决方法就是用链表的方式把冲突的联系人存储起来。例如,张三和张三三两个联系人,经过Hash算法计算后得到的数字一样,导致他们被映射到同一个页面上了。那么,HashMap就会把张三和张三三这两个联系人都存储在同一个链表中。

但是,随着联系人越来越多,同一个页面中存放的联系人也越来越多,链表就会变得很长,这样查找联系人的效率就会变慢。而为了提高查找效率,HashMap就会把链表变成红黑树,这样查找联系人的效率就会更高了。这就像我们在电话簿中找到联系人的电话号码,如果联系人名字相同的有很多个,我们就可以通过电话簿的索引来快速地找到联系人的电话号码,不需要一个一个地查找。

另外,HashMap还有一个很重要的参数,就是负载因子。负载因子是一个介于0和1之间的浮点数,它决定了HashMap在扩容之前内部数组的填充度。如果负载因子设置得太小,就会导致数组很快就被填满了,就需要扩容;如果负载因子设置得太大,虽然数组中还有很多位置可以使用,但由于链表或红黑树太长,查找效率却很低。因此,建议负载因子设为0.75,这个值经过多次实验得出,能够保证在时间和空间上达到一个平衡。

总的来说,HashMap是一种非常重要的数据结构,在很多场景下都会被使用到。有了对HashMap的深入了解,我们就可以在使用它的时候更加得心应手了。

🍊 数据结构

HashMap的底层数据结构是一个哈希表,它是由一个数组和若干个链表或红黑树构成的。下面分别介绍数组、链表和红黑树这三种数据结构的特点和用途。

🎉 1.数组

HashMap中的数组主要用于存储哈希表的节点,它是由若干个Node对象构成的。Node是HashMap中的一个内部类,它包含了键值对数据、哈希值、指向下一个节点的指针等信息。Node的定义如下:

// 定义了一个泛型的静态类 Node,实现了 Map.Entry 接口
static class Node<K,V> implements Map.Entry<K,V> {
    // hash 值
    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;   // hash 值
        this.key = key;     // 键
        this.value = value; // 值
        this.next = next;   // 链表的下一个节点
    }
    // 获取键
    public final K getKey() { return key; }
    // 获取值
    public final V getValue() { return value; }
    // 获取键值对的字符串表示形式
    public final String toString(){ return key + "=" + value; }
    // 获取哈希码
    public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); }
    // 设置新的值,返回旧的值
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    // 判断两个键值对是否相等
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

数组中的每个元素都是一个Node对象,它可以包含一个键值对,同时也可以是链表或红黑树的头节点。数组的长度会随着元素的不断插入而不断增加,当元素的数量超过数组容量的75%时,HashMap会进行扩容操作。

🎉 2.链表

在HashMap中,链表主要用于解决哈希冲突,当两个键值的哈希值相同时,它们会被存储在同一个位置的链表中。链表的每个节点都是一个Node对象,其中包含了键值对和指向下一个节点的指针。

链表的插入操作非常简单,只需要将新节点插入到链表的头部即可。但在查找操作中,由于需要遍历链表,所以效率较低。

为了提高查找效率,JDK1.8中增加了链表转红黑树的功能,当链表长度超过8时,会将其转换为红黑树。这个转换过程会使得查找的效率大大提高。

🎉 3.红黑树

红黑树是一种自平衡的二叉搜索树,它的左右子树高度差不超过1,能够保证查找、插入、删除等操作的时间复杂度为O(log n)。在HashMap中,红黑树用于取代链表,在链表长度超过8时,将链表转换为红黑树,以提高查找效率。

红黑树中的每个节点也是一个Node对象,它包含了键值对、哈希值等信息。与链表不同,红黑树中的节点是按照键值大小排列的,这使得查找操作可以具有O(log n)的时间复杂度。

当红黑树中的节点数小于6时,可以将其转换为链表,以节约内存。转换过程也是非常简单的,只需要按照键值顺序遍历树,然后重新将节点存储到原来的数组中即可。

🍊 哈希算法

首先,我们需要了解哈希算法。哈希算法是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。通俗点说,就是将任意大小的数据映射到固定大小的数据上。在HashMap中,哈希算法的作用是将键值映射到数组中的一个位置上。

在Java中,哈希算法的实现主要有两种:除留余数法和乘数法。

📝 1.除留余数法

除留余数法是一种较为常见的哈希算法,它的基本思想是将数据除以某个数后取余数作为它的哈希值。比如,我们可以选择数组长度作为除数,对于键k,我们可以用 k % 数组长度来得到它的哈希值。

具体来说,在HashMap中,哈希算法的实现是通过取键值k的hashCode()值,然后使用除留余数法将其映射到数组的某个索引位置上。HashMap中的哈希算法实现如下:

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

其中,key.hashCode()方法返回的是键值key的哈希值,h >>> 16是对h进行无符号右移16位操作。这里的h ^ (h >>> 16)是为了让哈希值的高位和低位都能够参与哈希运算。

📝 2.乘数法

乘数法是另一种常见的哈希算法,在Java中也有应用。它的基本思想是,将数据乘以一个小数,然后取结果的小数部分,再乘以数组长度,最后取整数部分作为哈希值。具体实现中,可以选择一个介于0和1之间的小数作为乘数,通常选择一个接近黄金比例的数。

在Java中,乘数法的实现可以参考HashMap中的hash函数:

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

这里的实现方式和除留余数法类似,不同之处在于乘数法需要再进行一次乘法运算。

🍊 HashMap版本问题

大家都知道,在Java中,HashMap是一种常用的数据结构,经常被用来存储键值对。其中,键是唯一的,值可以重复。但是在JDK1.7版本中,HashMap存在两个问题,会导致CPU利用率非常高,而在JDK1.8版本中,这两个问题得到了优化。

首先,我们来看看JDK1.7中HashMap的问题。在扩容时,HashMap需要进行rehash操作,这个过程非常消耗时间和空间,因为需要重新计算所有元素的hash值,并把它们放到新的位置上。其次,当并发执行扩容操作时,会出现链表元素倒置的情况,从而导致环形链和数据丢失等问题。

为了解决这些问题,在JDK1.8版本中,HashMap做了如下的优化。首先,在元素经过rehash之后,其位置要么是在原位置,要么是在原位置+原数组长度,这并不需要像旧版本的实现那样重新计算hash值,而只需要看看原来的hash值新增的那个bit是1还是0就好了。在数组的长度扩大到原来的2倍、4倍、8倍时,索引也会根据保留的二进制位上新增的1或0进行适当调整。

其次,在JDK1.8中,发生哈希碰撞时,插入元素不再采用头插法,而是直接插入链表尾部,从而避免了环形链表的情况。这样可以减少链表的长度,从而提高查询效率。

不过,在多线程环境下,还是会发生数据覆盖的情况。如果同时有线程A和线程B进行put操作,线程B在执行时已经插入了元素,而此时线程A获取到CPU时间片时会直接覆盖线程B插入的数据,从而导致数据覆盖和线程不安全的情况。

为了解决这个问题,可以采用synchronized关键字或者ConcurrentHashMap来实现线程安全的put操作。其中,synchronized关键字可以确保在put操作期间,没有其他线程可以修改HashMap中的数据。而ConcurrentHashMap则是使用一种类似于分段锁的方法来保证线程安全。每个段都有自己的锁,多个线程可以同时访问不同的段,从而提高并发能力。

综上所述,JDK1.8中对HashMap的改进很有价值,可以让我们更加高效地使用这个数据结构,并避免线程安全问题和CPU利用率过高的问题。

🍊 HashMap并发修改异常

在高并发场景下,使用HashMap可能会出现并发修改异常,这是因为多个线程同时竞争去修改同一个数据,导致了数据不一致的情况。例如,有两个线程A和B同时要往HashMap中添加元素,A线程添加了一个key-value,但是在value还没有添加完成时,B线程也来添加了同样的key-value,这时候就会覆盖掉A线程所添加的value,这就是出现并发修改异常的情况。

为了解决这个问题,有四种解决方案。第一种是使用HashTable,它是线程安全的,但是它把所有相关操作都加上了锁,因此在竞争激烈的并发场景中性能会非常差。第二种是使用工具类Collections.synchronizedMap(new HashMap<>()),将HashMap转化成同步的,但是同样会有性能问题。第三种解决方案是使用写时复制(CopyOnWrite)技术。在往容器中加元素时,不会直接添加到当前容器中,而是先将当前容器的元素复制出来放到一个新的容器中,然后在新的容器中添加元素。写操作完毕后,再将原来容器的引用指向新的容器。这种方法可以进行并发的读,不需要加锁。但是在复制的过程中会占用较多的内存,并且不能保证数据的实时一致性。

最后,使用ConcurrentHashMap则是一种比较推荐的解决方案。它使用了volatile,CAS等技术来减少锁竞争对性能的影响,避免了对全局加锁。在JDK1.7版本中,ConcurrentHashMap使用了分段锁技术,将数据分成一段一段的存储,并为每个段配备了锁。这样,当一个线程占用锁访问某一段数据时,其他段的数据也可以被其他线程访问,从而能够实现真正的并发访问。在JDK1.8版本中,ConcurrentHashMap内部使用了volatile来保证并发的可见性,并采用CAS来确保原子性,来解决了性能问题和数据一致性问题。

综上所述,当我们在高并发场景下需要使用HashMap时,我们可以采用四种方式来解决并发修改异常,其中ConcurrentHashMap是最优的解决方案。

🍊 HashMap影响HashMap性能的因素

HashMap是Java中经常使用的一种数据结构,它可以存储键值对(key-value),并在O(1)的时间内快速访问到对应的value值。但是,在使用HashMap时有两个关键因素会影响它的性能,分别是加载因子和初始容量。

加载因子用于确定HashMap中存储的数据量,并且默认加载因子为0.75。这个数值是经过充分考虑得出的,如果加载因子比较大,扩容发生的频率就会比较低,而浪费的空间会比较小,但是发生hash冲突的几率会比较大。举个例子,如果加载因子为1,HashMap长度为128,实际存储元素的数量在64至128之间,这个时间段发生hash冲突比较多,会影响性能。

相反,如果加载因子比较小,扩容发生的频率会比较高,浪费的空间也会比较多,但是发生hash冲突的几率会比较小。比如,如果加载因子为0.5,HashMap长度为128,当数量达到65的时候会触发扩容,扩容后为原理的256,256里面只存储了65个,浪费了。

为了平衡这两者,我们可以取一个平均数0.75作为加载因子,这样既可以减少hash冲突的几率,又可以尽可能地利用空间。

另一个影响HashMap性能的关键因素是初始容量,它始终为2的n次方,可以是16、32、64等这样的数字。即使你传递的值是13,数组长度也会变成16,因为它会选择最近的2的n次方的数。

在HashMap中,使用(hash值 &(长度-1))的二进制进行&运算来得到元素在数组中的下标。这样做可以保证运算得到的值可以落到数组的每一个下标上,避免了某些下标永远没有元素的情况。

举个例子,如果我有一个HashMap,容量为16,我的hash值是11001110 11001111 00010011 11110001(hash值),然后要进行&运算,运算的值是00000000 00000000 00000000 00001111(16-1的2进制)。这个值是16-1的2进制表示。然后,进行&运算得到的结果是00000000 00000000 00000000 00000001。这个运算的意思是,我把hash值的2进制的后4位和1111进行比较,然后,我的hash值的后4位的范围是0000-1111之间,这样我就可以与上1111,最后的值就可以在0000-1111之间,也就是0-15之间。

这样可以保证运算后的值可以落到数组的每一个下标中。如果数组长度不是2的幂次,后四位就不可能是1111,这样如果我用0000~1111的一个数和有可能不是1111的数进行&运算,那么就有可能导致数组的某些位下标永远不会有值,这样就无法保证运算后的值可以落在数组的每个下标上面。

因此,在使用HashMap时,要注意设置合适的加载因子和初始容量,才能保证它的性能最优。

🍊 HashMap使用优化

你想要优化 HashMap 的使用效率,可能就需要一些技巧了。那么,我个人总结了五个优化建议,帮助你更好地使用 HashMap。

🎉 1. 使用短String或者Integer作为键

首先,我们知道 HashMap 中的键值对是通过键来存储和查找的。因此,选择一个合适的键是非常重要的。我建议使用短 String、Integer 这些类作为键,特别是 String,因为它是不可变的(final),已经重写了 equals 和 hashCode 方法,符合 HashMap 计算 hashCode 的不可变性要求,可以最大限度地减少碰撞的出现。这样,我们可以有效地提高 HashMap 的查询效率。

🎉 2. 使用迭代器遍历entrySet

第二,建议不要使用 for 循环遍历 Map,而是使用迭代器遍历 entrySet,因为在各个数量级别迭代器遍历效率都比较高。举个例子,假如我们有一个 HashMap 存储了 10000 个键值对,我们想要遍历这个 Map,一共有两种方式:

第一种,使用 for 循环遍历 Map:

Map<String, Integer> map = new HashMap<String, Integer>();
// ...添加键值对...
for (String key : map.keySet()) {
    Integer value = map.get(key);
    // 处理 value
}

第二种,使用迭代器遍历 entrySet:

Map<String, Integer> map = new HashMap<String, Integer>();
// ...添加键值对...
Iterator<Map.Entry<String, Integer>> iter = map.entrySet().iterator();
while (iter.hasNext()) {
    Map.Entry<String, Integer> entry = iter.next();
    String key = entry.getKey();
    Integer value = entry.getValue();
    // 处理 value
}

我们可以看到,第二种方式使用了迭代器,而不是 for 循环。这是因为在各个数量级别迭代器遍历效率都比较高,相比之下,for 循环则会稍慢一些。

🎉 3. 使用线程安全的 ConcurrentHashMap 或者迭代器 iterator.remove 方法来删除元素

第三,建议使用线程安全的 ConcurrentHashMap 来删除 Map 中的元素,或者在迭代器 Iterator 遍历时,使用迭代器 iterator.remove() 方法来删除元素。不可以使用 for 循环遍历删除,否则会产生并发修改异常 CME。如果我们需要从 HashMap 中删除元素,我们可以使用以下代码:

// 在迭代器 Iterator 遍历时使用 iterator.remove() 方法来删除元素
Iterator<Map.Entry<String, Integer>> iter = map.entrySet().iterator();
while (iter.hasNext()) {
    Map.Entry<String, Integer> entry = iter.next();
    if (entry.getValue() == 0) {
        iter.remove(); // 在迭代器中删除元素
    }
}
// 使用 ConcurrentHashMap 来删除元素
ConcurrentMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
map.put("key", 1);
map.remove("key");

🎉 4. 在设定初始大小时要考虑加载因子的存在

第四,建议在设定初始大小时要考虑加载因子的存在,最好估算存储的大小。我们可以使用 Maps.newHashMapWithExpectedSize(预期大小) 来创建一个 HashMap,Guava 会帮我们完成计算过程,同时考虑设定初始加载因子。加载因子越大,hash 冲突的概率就越高。因此,建议在指定初始大小时,要考虑加载因子的存在,尽量估算存储大小,以减少冲突的发生。

Map<String, Integer> map = Maps.newHashMapWithExpectedSize(10000);

🎉 5. 适当加大初始大小,同时减少加载因子

最后,如果 Map 是长期存在而 key 又是无法预估的,那就可以适当加大初始大小,同时减少加载因子,降低冲突的机率。在长期存在的 Map 中,降低冲突概率和减少比较的次数更加重要。我们可以使用以下代码来设置初始大小和加载因子:

Map<String, Integer> map = new HashMap<String, Integer>(10000, 0.75f);

综上所述,以上就是个人总结的五个优化建议,希望能给大家带来帮助,提高 HashMap 的使用效率。


相关文章
|
10月前
|
安全 算法 网络协议
解析:HTTPS通过SSL/TLS证书加密的原理与逻辑
HTTPS通过SSL/TLS证书加密,结合对称与非对称加密及数字证书验证实现安全通信。首先,服务器发送含公钥的数字证书,客户端验证其合法性后生成随机数并用公钥加密发送给服务器,双方据此生成相同的对称密钥。后续通信使用对称加密确保高效性和安全性。同时,数字证书验证服务器身份,防止中间人攻击;哈希算法和数字签名确保数据完整性,防止篡改。整个流程保障了身份认证、数据加密和完整性保护。
|
9月前
|
机器学习/深度学习 数据可视化 PyTorch
深入解析图神经网络注意力机制:数学原理与可视化实现
本文深入解析了图神经网络(GNNs)中自注意力机制的内部运作原理,通过可视化和数学推导揭示其工作机制。文章采用“位置-转移图”概念框架,并使用NumPy实现代码示例,逐步拆解自注意力层的计算过程。文中详细展示了从节点特征矩阵、邻接矩阵到生成注意力权重的具体步骤,并通过四个类(GAL1至GAL4)模拟了整个计算流程。最终,结合实际PyTorch Geometric库中的代码,对比分析了核心逻辑,为理解GNN自注意力机制提供了清晰的学习路径。
632 7
深入解析图神经网络注意力机制:数学原理与可视化实现
|
10月前
|
机器学习/深度学习 算法 数据挖掘
解析静态代理IP改善游戏体验的原理
静态代理IP通过提高网络稳定性和降低延迟,优化游戏体验。具体表现在加快游戏网络速度、实时玩家数据分析、优化游戏设计、简化更新流程、维护网络稳定性、提高连接可靠性、支持地区特性及提升访问速度等方面,确保更流畅、高效的游戏体验。
238 22
解析静态代理IP改善游戏体验的原理
|
9月前
|
机器学习/深度学习 缓存 自然语言处理
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
Tiktokenizer 是一款现代分词工具,旨在高效、智能地将文本转换为机器可处理的离散单元(token)。它不仅超越了传统的空格分割和正则表达式匹配方法,还结合了上下文感知能力,适应复杂语言结构。Tiktokenizer 的核心特性包括自适应 token 分割、高效编码能力和出色的可扩展性,使其适用于从聊天机器人到大规模文本分析等多种应用场景。通过模块化设计,Tiktokenizer 确保了代码的可重用性和维护性,并在分词精度、处理效率和灵活性方面表现出色。此外,它支持多语言处理、表情符号识别和领域特定文本处理,能够应对各种复杂的文本输入需求。
1120 6
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
|
10月前
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
648 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
9月前
|
传感器 人工智能 监控
反向寻车系统怎么做?基本原理与系统组成解析
本文通过反向寻车系统的核心组成部分与技术分析,阐述反向寻车系统的工作原理,适用于适用于商场停车场、医院停车场及火车站停车场等。如需获取智慧停车场反向寻车技术方案前往文章最下方获取,如有项目合作及技术交流欢迎私信作者。
651 2
|
11月前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
12330 46
|
10月前
|
Java 数据库 开发者
详细介绍SpringBoot启动流程及配置类解析原理
通过对 Spring Boot 启动流程及配置类解析原理的深入分析,我们可以看到 Spring Boot 在启动时的灵活性和可扩展性。理解这些机制不仅有助于开发者更好地使用 Spring Boot 进行应用开发,还能够在面对问题时,迅速定位和解决问题。希望本文能为您在 Spring Boot 开发过程中提供有效的指导和帮助。
1199 12
|
10月前
|
开发框架 监控 JavaScript
解锁鸿蒙装饰器:应用、原理与优势全解析
ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。
288 2
|
9月前
|
负载均衡 JavaScript 前端开发
分片上传技术全解析:原理、优势与应用(含简单实现源码)
分片上传通过将大文件分割成多个小的片段或块,然后并行或顺序地上传这些片段,从而提高上传效率和可靠性,特别适用于大文件的上传场景,尤其是在网络环境不佳时,分片上传能有效提高上传体验。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS