Java中HashMap、HashTable与HashSet的深度解析:区别、联系与实践指南

引言

在Java集合框架中,HashMap、HashTable与HashSet是最常用的哈希型数据结构。它们因高效的查找、插入与删除性能(平均时间复杂度O(1)),广泛应用于缓存设计、数据去重、键值映射等场景。但三者在设计定位、线程安全性、底层实现等方面存在显著差异,若使用不当可能导致性能问题或逻辑错误。本文将从基础概念出发,结合源码与典型代码,系统解析三者的核心特性,帮助开发者深入理解其适用场景。


一、基础概念铺垫:哈希表与集合的底层逻辑

1.1 Map与Set的核心定义

Java集合框架中,MapSet是两大核心接口类型:

  • Map(键值对映射表):存储Key-Value键值对,键(Key)具有唯一性,值(Value)可重复。典型实现包括HashMap、HashTable、TreeMap等。
  • Set(唯一元素集合):存储无序且不重复的元素,本质是“只有键的Map”。典型实现包括HashSet、TreeSet、LinkedHashSet等。

二者的核心差异在于:Map关注键与值的映射关系,Set关注元素的唯一性。但从底层实现看,HashSet与HashMap存在强关联(后文详述)。

1.2 哈希表的底层原理

HashMap、HashTable与HashSet均基于**哈希表(Hash Table)实现。哈希表的核心思想是通过哈希函数(Hash Function)**将元素的键(或元素本身)映射到数组的某个位置(桶,Bucket),从而实现快速的增删查操作。其关键步骤如下:

  1. 哈希计算:通过键的hashCode()方法计算哈希值,再通过(n-1) & hash(n为数组长度)将哈希值映射到数组索引(桶位置)。
  2. 冲突处理:不同键可能映射到同一桶(哈希冲突),Java采用链地址法解决:每个桶存储一个链表(或红黑树),冲突元素按顺序链入。
  3. 扩容机制:当元素数量超过容量×加载因子(阈值)时,哈希表会扩容(数组长度翻倍),并重新哈希所有元素到新数组。

关键概念

  • 容量(Capacity):哈希表底层数组的长度,默认16(HashMap/HashSet)或11(HashTable)。
  • 加载因子(Load Factor):触发扩容的阈值比例,默认0.75(三者均如此)。
  • 阈值(Threshold):容量×加载因子,超过则触发扩容。

二、HashMap深度解析:高效的非线程安全映射表

2.1 核心特性

HashMap是Java中使用最广泛的Map实现,自JDK1.2引入,其核心特性如下:

  • 线程非安全:未使用同步机制,单线程环境下性能最优。
  • 支持null键值:允许1个null键(因键唯一)和任意数量的null值。
  • 底层结构进化:JDK1.7及之前使用“数组+链表”;JDK1.8优化为“数组+链表+红黑树”,当链表长度≥8且数组长度≥64时,链表转为红黑树(查询时间复杂度从O(n)降至O(logn))。

2.2 关键参数与底层细节

2.2.1 默认参数
  • 初始容量:16(DEFAULT_INITIAL_CAPACITY)。
  • 加载因子:0.75(DEFAULT_LOAD_FACTOR)。
  • 红黑树转换阈值:8(TREEIFY_THRESHOLD)。
  • 红黑树退化为链表阈值:6(UNTREEIFY_THRESHOLD)。
  • 触发树化的最小数组长度:64(MIN_TREEIFY_CAPACITY)。
2.2.2 哈希计算逻辑

HashMap通过以下步骤计算桶索引:

// 键的原始hashCode()
int hash = key.hashCode();
// 高位异或(扰动函数):将高16位与低16位异或,减少哈希冲突
int扰动Hash = hash ^ (hash >>> 16);
// 计算桶索引(n为当前数组长度,必为2的幂次)
int bucketIndex = (n - 1) & 扰动Hash;

扰动函数的设计是为了让哈希值的高位参与索引计算(因数组长度n较小,直接取模会丢失高位信息),从而降低冲突概率。

2.3 核心操作代码示例

2.3.1 基础操作
import java.util.HashMap;
import java.util.Map;public class HashMapDemo {public static void main(String[] args) {// 1. 创建HashMap(默认初始容量16,加载因子0.75)Map<String, Integer> map = new HashMap<>();// 2. 插入键值对(允许null键和null值)map.put("apple", 10);    // 常规键值map.put(null, 0);        // null键map.put("banana", null); // null值// 3. 查询值Integer appleCount = map.get("apple");  // 10Integer nullKeyCount = map.get(null);   // 0// 4. 替换值(put()会覆盖旧值)map.put("apple", 20);  // 原10被替换为20// 5. 删除键值对map.remove("banana");  // 移除null值的键// 6. 遍历键值对(推荐entrySet())for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());}}
}
2.3.2 自定义键类型(重写hashCode与equals)

若使用自定义类作为键,需重写hashCode()equals()方法,否则无法正确判断键的唯一性。例如:

class Student {private String id;private String name;public Student(String id, String name) {this.id = id;this.name = name;}// 重写hashCode:基于id计算(保证相同id的对象哈希值相同)@Overridepublic int hashCode() {return id.hashCode();}// 重写equals:仅比较id(保证相同id的对象视为同一键)@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Student student = (Student) o;return id.equals(student.id);}
}// 使用示例
Map<Student, String> studentMap = new HashMap<>();
Student s1 = new Student("001", "Alice");
Student s2 = new Student("001", "Bob"); // id相同,视为同一键
studentMap.put(s1, "Math");
studentMap.put(s2, "Physics"); // 覆盖s1的值为"Physics"
System.out.println(studentMap.size()); // 输出1(键唯一)

2.4 JDK1.8的关键优化:链表转红黑树

在JDK1.7及之前,HashMap的底层结构是“数组+链表”。当链表过长时(如长度10),查询时间复杂度退化为O(n)。JDK1.8引入红黑树优化:

  • 触发树化条件:链表长度≥8且数组长度≥64。
    (选择8的原因:链表长度符合泊松分布,长度8的概率仅0.00000006,属于小概率事件;若频繁触发树化,说明哈希函数设计不合理。)
  • 树化优势:红黑树的查找、插入、删除时间复杂度为O(logn),显著优于链表的O(n)。
  • 退化条件:当树的大小≤6时,红黑树退化为链表(避免频繁树化与退化的性能损耗)。

三、HashTable深度解析:线程安全的“老派”映射表

3.1 核心特性与历史背景

HashTable是JDK1.0的“古董级”类,早于HashMap(JDK1.2)。其核心特性如下:

  • 线程安全:所有方法均用synchronized修饰(全局锁),保证多线程操作的原子性。
  • 不支持null键值put(null, value)put(key, null)会抛出NullPointerException
  • 底层结构落后:JDK1.8仍使用“数组+链表”,无红黑树优化。
  • 设计定位过时:因全局锁性能低下,已被ConcurrentHashMap(JDK1.5引入)替代。

3.2 关键参数与底层差异

3.2.1 默认参数
  • 初始容量:11(DEFAULT_INITIAL_CAPACITY)。
  • 加载因子:0.75(与HashMap一致)。
  • 扩容机制:旧容量×2+1(如初始11→扩容后23→47…),而HashMap是旧容量×2(始终为2的幂次)。
3.2.2 哈希计算逻辑

HashTable的哈希计算未做高位扰动,直接使用键的hashCode()取模:

int hash = key.hashCode();
int bucketIndex = (hash & 0x7FFFFFFF) % table.length; // 取模避免负数索引

这种方式在数组长度非2的幂次时,哈希冲突概率高于HashMap的(n-1) & hash(当n是2的幂次时,(n-1) & hash等价于hash % n,但位运算更快)。

3.3 核心操作代码示例

3.3.1 基础操作(注意null限制)
import java.util.Hashtable;public class HashTableDemo {public static void main(String[] args) {// 1. 创建HashTable(默认初始容量11,加载因子0.75)Hashtable<String, Integer> table = new Hashtable<>();// 2. 插入键值对(不允许null键或null值)table.put("apple", 10);    // 合法// table.put(null, 0);     // 抛出NullPointerException// table.put("banana", null); // 抛出NullPointerException// 3. 查询值(与HashMap类似)Integer appleCount = table.get("apple"); // 10// 4. 线程安全演示(多线程插入)Runnable task = () -> {for (int i = 0; i < 1000; i++) {table.put(Thread.currentThread().getName() + "-" + i, i);}};Thread t1 = new Thread(task, "Thread-1");Thread t2 = new Thread(task, "Thread-2");t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("最终大小:" + table.size()); // 输出2000(无数据丢失)}
}
3.3.2 性能对比(HashTable vs HashMap)

在单线程环境下,HashTable的全局锁会带来显著性能损耗。测试插入100万条数据:

// 单线程插入测试(时间单位:ms)
long start = System.currentTimeMillis();
Map<String, Integer> hashMap = new HashMap<>();
for (int i = 0; i < 1_000_000; i++) {hashMap.put("key-" + i, i);
}
System.out.println("HashMap耗时:" + (System.currentTimeMillis() - start)); // ~50msstart = System.currentTimeMillis();
Hashtable<String, Integer> hashTable = new Hashtable<>();
for (int i = 0; i < 1_000_000; i++) {hashTable.put("key-" + i, i);
}
System.out.println("HashTable耗时:" + (System.currentTimeMillis() - start)); // ~120ms

可见,单线程下HashTable性能约为HashMap的40%。

3.4 为什么HashTable被淘汰?

HashTable的全局锁设计导致多线程竞争时,所有操作需串行执行(即使操作不同桶)。例如,线程1操作桶A,线程2操作桶B,仍需等待锁释放。而ConcurrentHashMap(JDK1.8)采用CAS+ synchronized细粒度锁(仅锁定链表头或红黑树根节点),多线程性能提升10倍以上。因此,HashTable仅用于兼容旧代码,新场景应优先选择ConcurrentHashMap


四、HashSet深度解析:基于HashMap的唯一元素集合

4.1 核心特性

HashSet是Set接口的实现类,其核心特性如下:

  • 元素唯一性:依赖HashMap的键唯一性,通过add(E e)调用HashMap.put(e, PRESENT)实现(PRESENT是静态常量)。
  • 无序性:元素存储顺序与插入顺序无关(区别于LinkedHashSet的有序性)。
  • 线程非安全:底层依赖HashMap,未做同步处理。
  • 支持null元素:允许存储1个null(因HashMap允许1个null键)。

4.2 与HashMap的依赖关系(源码级解析)

查看HashSet的JDK源码(JDK1.8):

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {// 底层依赖HashMap,元素作为键,值固定为PRESENTprivate transient HashMap<E, Object> map;private static final Object PRESENT = new Object();// 构造方法:初始化HashMappublic HashSet() {map = new HashMap<>();}// add方法:调用HashMap的put,若键已存在则返回旧值(null),否则返回nullpublic boolean add(E e) {return map.put(e, PRESENT) == null;}// contains方法:调用HashMap的containsKeypublic boolean contains(Object o) {return map.containsKey(o);}// remove方法:调用HashMap的removepublic boolean remove(Object o) {return map.remove(o) == PRESENT;}
}

可见,HashSet完全是HashMap的“包装器”,其所有操作均委托给底层的HashMap实例,元素作为键存储,值统一为静态的PRESENT对象。

4.3 核心操作代码示例

4.3.1 基础操作
import java.util.HashSet;
import java.util.Set;public class HashSetDemo {public static void main(String[] args) {// 1. 创建HashSet(底层是HashMap)Set<String> set = new HashSet<>();// 2. 添加元素(允许1个null)set.add("apple");set.add("banana");set.add(null); // 合法set.add("apple"); // 重复元素,添加失败// 3. 查询元素是否存在boolean hasApple = set.contains("apple"); // trueboolean hasNull = set.contains(null);     // true// 4. 删除元素set.remove("banana");// 5. 遍历元素(迭代器或增强for)for (String element : set) {System.out.println(element); // 输出:null, apple(顺序不确定)}}
}
4.3.2 元素唯一性的实现原理

HashSet的元素唯一性由HashMap的键唯一性保证,依赖hashCode()equals()方法:

  • 若两个元素的hashCode()不同,直接存储在不同桶,视为不同元素。
  • hashCode()相同(哈希冲突),则通过equals()比较内容:若返回true,视为同一元素,不重复存储;否则链入同一桶的链表/红黑树。

4.4 性能与HashMap的一致性

由于HashSet的所有操作均委托给HashMap,其时间复杂度与HashMap完全一致:

  • 插入(add()):O(1)(平均),O(n)(链表)或O(logn)(红黑树)(最坏)。
  • 查询(contains()):O(1)(平均),O(n)或O(logn)(最坏)。
  • 删除(remove()):O(1)(平均),O(n)或O(logn)(最坏)。

五、三者的区别对比:从设计到实现的全方位解析

为更清晰对比三者差异,我们从10个维度总结如下表:

维度HashMapHashTableHashSet
线程安全性非线程安全(无同步机制)线程安全(所有方法synchronized修饰,全局锁)非线程安全(依赖底层HashMap)
是否支持null键/值支持null键(1个)、null值(任意)不支持null键或null值(抛NPE)支持null元素(1个,作为键存储)
底层数据结构(JDK1.8+)数组+链表+红黑树(链表≥8且数组≥64时树化)数组+链表(无红黑树优化)底层为HashMap(数组+链表+红黑树)
初始容量默认16(DEFAULT_INITIAL_CAPACITY默认11(DEFAULT_INITIAL_CAPACITY默认由底层HashMap决定(即16)
扩容机制旧容量×2(始终为2的幂次)旧容量×2+1(如11→23→47…)同HashMap(旧容量×2)
哈希计算逻辑扰动函数(高16位异或低16位)+(n-1)&hash直接取模(hash % table.length同HashMap(依赖键的哈希计算)
适用场景单线程键值映射(缓存、配置存储等)兼容旧代码(新场景推荐ConcurrentHashMap单线程元素去重、唯一性校验
JDK版本引入JDK1.2JDK1.0(古董级)JDK1.2
父类/接口继承AbstractMap,实现Map接口继承已过时的Dictionary,实现Map接口继承AbstractSet,实现Set接口
性能特点单线程性能最优(无锁开销)单线程性能差(全局锁);多线程仍低效(锁竞争)与HashMap一致(操作委托给底层HashMap)

六、实践指南:如何选择三者?

6.1 单线程场景:优先HashMap与HashSet

  • 键值映射需求:直接使用HashMap。其无锁设计、红黑树优化及支持null的特性,完美适配缓存、配置存储、对象属性映射等场景。
    示例:缓存用户信息(userId为键,User对象为值)。
  • 元素唯一性需求:使用HashSet。通过包装HashMap实现,代码简洁且性能与HashMap一致。
    示例:去除列表中的重复元素(new HashSet<>(list)后转回列表)。

6.2 多线程场景:避免HashTable,选择ConcurrentHashMap

  • 键值映射需求:优先ConcurrentHashMap(JDK1.5+)。其采用CAS+细粒度锁(仅锁定链表头或红黑树根节点),多线程性能远超HashTable的全局锁。
    示例:多线程统计日志中的IP访问次数(ConcurrentHashMap<IP, Integer>)。
  • 元素唯一性需求:若需线程安全,可通过Collections.synchronizedSet(new HashSet<>())包装,或直接使用CopyOnWriteArraySet(写时复制,适合读多写少场景)。

6.3 兼容旧代码:仅当必要时使用HashTable

HashTable仅推荐用于维护JDK1.0时代的遗留代码。若项目需兼容极低版本JDK(如无ConcurrentHashMap),且必须保证线程安全,可谨慎使用;否则应升级为更高效的并发容器。


结语

HashMap、HashTable与HashSet是Java哈希型数据结构的核心成员,其设计差异本质上源于线程安全需求功能定位的不同:

  • HashMap以性能为优先,通过红黑树优化和无锁设计成为单线程键值映射的首选;
  • HashTable因全局锁的历史局限性逐渐被淘汰,仅作为兼容方案存在;
  • HashSet通过“键唯一”的HashMap特性,简洁实现了元素去重与唯一性校验。

理解三者的底层逻辑与适用场景,是写出高效、健壮Java代码的关键。开发者应根据具体需求(线程安全、数据类型、性能要求)选择合适的容器,避免“为了用而用”的错误实践。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94421.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94421.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/94421.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

互联网大厂Java面试实战:核心技术栈与场景化提问解析(含Spring Boot、微服务、测试框架等)

互联网大厂Java面试实战&#xff1a;核心技术栈与场景化提问解析 本文通过模拟面试官与求职者谢飞机的对话&#xff0c;深入探讨互联网大厂Java开发的核心技术栈面试问题&#xff0c;涵盖Java SE、Spring生态、微服务、大数据等多个领域&#xff0c;结合音视频、电商、AIGC等业…

人工智能-python-深度学习-参数初始化与损失函数

文章目录参数初始化与损失函数一、参数初始化1. 固定值初始化1.1 全零初始化1.2 全1初始化1.3 任意常数初始化2. 随机初始化2.1 均匀分布初始化2.2 正态分布初始化3. Xavier初始化4. He初始化5. 总结二、损失函数1. 线性回归损失函数1.1 MAE&#xff08;Mean Absolute Error&am…

Android Glide常见问题解决方案:从图片加载到内存优化

全面总结Glide使用中的典型问题与解决方案&#xff0c;助力提升应用性能与用户体验作为Android开发中最流行的图片加载库之一&#xff0c;Glide以其简单易用的API和强大的功能深受开发者喜爱。然而&#xff0c;在实际使用过程中&#xff0c;我们往往会遇到各种问题&#xff0c;…

linux系统ollama监听0.0.0.0:11434示例

docker应用如dify访问本地主机部署的ollama&#xff0c;base_url不管配"http://localhost:11434"&#xff0c;还是"http://host_ip:11434"都会报错。这是因为1&#xff09;docker容器访问http://localhost:11434&#xff0c;其实访问的是docker容器自身的服…

Java微服务AI集成指南:LangChain4j vs SpringAI

今天想再完善一下做的微服务项目&#xff0c;想着再接入一个人工客服&#xff0c;于是学习了一下langchan4j的内容&#xff0c;未完一、技术定位辨析&#xff1a;LangChain4j vs Spring AI vs OpenAIOpenAI&#xff1a;AI模型提供商 提供大语言模型API&#xff08;如GPT-4o&…

华为光学设计面试题

16. 题目&#xff1a;设计一个用于机器视觉检测的光学系统时&#xff0c;如何保证在高速运动下成像的清晰度和稳定性&#xff1f;(出处&#xff1a;华为智能制造光学检测项目组招聘面试题)17. 题目&#xff1a;请说明在光学系统设计中&#xff0c;如何权衡景深和分辨率的关系&a…

vue3和react的异同点

这是一个前端领域非常核心的话题。Vue 3 和 React 都是极其优秀的现代前端框架&#xff0c;它们在理念和实现上既有相似之处&#xff0c;也有显著区别。 下面我将从多个维度详细对比它们的异同点。核心哲学与设计理念特性Vue 3React设计理念渐进式框架与 “救世主”声明式 UI 库…

assetbuddle hash 比对

1.测试 &#xff1a;当在预设上的数据有修改时&#xff0c;生成的ab也会有修改&#xff0c;具体到某个ab的.manifest里会有相应的变化&#xff0c;AssetFileHash 会修改 如图所示&#xff1a; ManifestFileVersion: 0 CRC: 2818930197 Hashes: AssetFileHash: serializedVersio…

Spring Boot `@Configuration` 与 `@Component` 笔记

Spring Boot Configuration 与 Component 笔记 1️⃣ 基本概念注解作用是否有代理适用场景Component标记普通组件&#xff0c;将类交给 Spring 容器管理❌ 没有 CGLIB 代理普通 Bean&#xff0c;工具类、过滤器、监听器等Configuration标记配置类&#xff0c;用来声明 Bean✅ 有…

二、JVM 入门——(三)栈

栈的定义 栈也是一块区域&#xff0c;用来存放数据的。栈也叫栈内存&#xff0c;主管Java程序的运行。 栈是私有的&#xff0c;是在线程创建时创建&#xff0c;所以它的生命期是跟随线程的生命期&#xff0c;线程结束栈内存也就释放。 因此对于栈来说不存在垃圾回收问题&…

深度学习入门第一课——神经网络实现手写数字识别

昨天我们讲了深度学习的大致框架&#xff0c;下面我们用深度学习网络来实现一个小项目——手写数字识别。完整代码import torch from torch import nn from torch.utils.data import DataLoader from torchvision import datasets from torchvision.transforms import ToTensor…

Vue中的scoped属性

理解&#xff1a; 在 .vue 文件中&#xff0c;scoped 是 <style> 标签的一个属性&#xff0c;作用是让样式只作用于当前组件&#xff0c;避免样式污染其他组件 scoped 让样式只在自己的组件内生效&#xff0c;不会影响到其他组件的同名元素 举例 没有 scoped 的情况&…

S2B2B系统哪个好,商淘云、数商云、金蝶云苍穹供应链批发哪个比较靠谱

在数字化商业浪潮汹涌的当下&#xff0c;S2B2B系统已成为众多企业优化供应链、提升运营效率、拓展业务版图的关键利器。从大型企业的复杂产业链协同&#xff0c;到中小企业对上下游资源的整合与把控&#xff0c;S2B2B系统的身影无处不在。但面对市场上层出不穷的各类S2B2B系统&…

利用3台机器搭建Kubernetes集群

文章目录1 机器准备修改主机名(可选)配置IP地址和主机名映射关闭防火墙关闭swap2 安装docker修改并更新软件源安装docker设置路由转发规则&#xff0c;并使配置生效安装 kubelet&#xff0c;kubeadm&#xff0c;kubectl3 初始化集群4 集群初始化5 work节点加入6 部署网络插件本…

C6.3:发射结交流电阻

当一个小交流电压加在发射结上&#xff0c;会产生发射极交流电流&#xff0c;该交流电流的大小取决于Q点位置&#xff0c;而曲线是弯曲的&#xff0c;所以Q点在曲线上的位置越高&#xff0c;发射极交流电流的峰值越大。发射极总电流是直流分量和交流分量叠加而成&#xff0c;即…

python中生成器

生成器是根据算法生成数据的一种机制&#xff0c;每次调用生成器只生成一个值&#xff0c;可以节省大量内存生成器推导式gg ( i * 2 for i in range(5))print(gg)print(next(gg)) # 0 print(next(gg)) # 2 print(next(gg)) # 4 print(next(gg)) # 6 print(next(gg)) # 8yield…

【RAGFlow代码详解-28】部署和基础设施

Docker 部署架构 RAGFlow 使用多阶段 Docker 构建过程和 Docker Compose 编排进行部署。该系统支持完整和轻量级部署变体。 多阶段 Docker 构建构建过程由 Dockerfile 2-214中定义的三个阶段组成&#xff1a; 基础阶段 &#xff1a;使用系统依赖项、ML 模型和运行时环境设置 Ub…

【prism】Prism 弹窗在 ViewModel 中控制大小的实践总结

好 &#x1f44d; 这个思路非常适合写成一篇 实用型博客&#xff0c;主题就是 “Prism 弹窗如何在 ViewModel 中控制窗口大小”。我帮你整理了一篇文章草稿&#xff0c;你可以直接拿去修改/发布。Prism 弹窗在 ViewModel 中控制大小的实践总结 在 WPF 中使用 Prism 的 IDialogS…

项目管理进阶——研发项目立项管理制度

为使公司研究开发(以下简称研发)项目的管理工作规范化、程序化、充分调度研发人员的积极性,提高研发成果的产出率和成果转化率,特制定管理办法。 一、 研发项目的立项: 原则上公司部设立基础研究项目。研发项目的重点放在符合市场需要。能很快转化成产品,或对现有生产工…

CMake构建学习笔记20-iconv库的构建

1. 构建 iconv是一个用于在不同字符编码&#xff08;如 UTF-8、GBK、ISO-8859-1 等&#xff09;之间进行转换的开源库。笔者在《c中utf8字符串和gbk字符串的转换》这篇文章中介绍过如何在Windows下实现utf8字符串和gbk字符串的转换&#xff0c;不过该实现是基于Win32 API的&am…