← 返回题目列表

Java 的 char 是几个字节?为什么会出现乱码?Unicode 和 UTF-8 是什么关系?

中等 第 25 / 32 题 更新于 2026/07/27
char字符编码UnicodeUTF-8

简化版

Java 的 char2 个字节(16 位),因为 Java 内部用 UTF-16 编码字符(一个 char 存一个 UTF-16 码元)。要分清几个概念:Unicode 是「字符集」——给世界上每个字符分配一个唯一编号(码点,如 ‘中’ = U+4E2D);UTF-8/UTF-16 是「编码方式」——把码点转成实际字节(UTF-8 是变长 1~4 字节、UTF-16 是 2 或 4 字节)。乱码的根本原因是「编码和解码用了不同的字符编码」——比如用 UTF-8 编码存的字节,用 GBK 去解码,就乱了。所以处理字符串、读写文件、网络传输时要统一编码(推荐 UTF-8)。另外一个坑:char 只有 2 字节,存不下 emoji 等「增补字符」(码点 > U+FFFF),它们要用两个 char(代理对) 表示。

详细版

三个核心概念的区别

概念是什么例子
字符集(Unicode)给每个字符分配唯一编号(码点)‘中’ → U+4E2D、‘A’ → U+0041、’😀’ → U+1F600
编码(UTF-8/16/32)把码点转成实际字节的规则UTF-8 变长 1~4 字节、UTF-16 定长 2 或 4 字节
字节流实际存储/传输的 0/1’中’ 的 UTF-8 编码是 3 字节 E4 B8 AD

为什么 Java 的 char 是 2 字节

Java 内部用 UTF-16 存字符串(char[] / 早期)
UTF-16:基本平面字符(U+0000~U+FFFF,覆盖常用字符)用 1 个 char(2字节)表示
所以 char = 2 字节 = 一个 UTF-16 码元
但:char 只有 2 字节,存不下码点 > U+FFFF 的字符(emoji、生僻字)
  → 这些"增补字符"要用 2 个 char(代理对 surrogate pair)表示

乱码的原因与解决

// 乱码的本质:编码和解码用了不同的字符集
byte[] bytes = "中文".getBytes("UTF-8");     // 用 UTF-8 编码成字节
String s = new String(bytes, "GBK");         // 用 GBK 解码 → 乱码!

// 正确:编码和解码用同一个字符集
String s2 = new String("中文".getBytes("UTF-8"), "UTF-8");  // ✓ 不乱

代理对(emoji 的坑)

String emoji = "😀";              // 码点 U+1F600,超出 U+FFFF
System.out.println(emoji.length());        // 2!不是 1——用了 2 个 char(代理对)
System.out.println(emoji.codePointCount(0, emoji.length()));  // 1——按码点算才是 1 个字符

⚠️ String.length() 返回的是 char 的个数(UTF-16 码元数)不是「字符数」——对于 emoji、部分生僻字(增补字符,用代理对表示),一个字符占 2 个 char,length() 会返回 2。所以「统计字符个数」不能直接用 length(),要用 codePointCount();截取字符串也要小心别把代理对从中间截断(截断后是无效字符)。

完整版教学

一、先分清字符集和编码:两个不同的层次

字符处理里最容易混的是「字符集」和「编码」,它们是两个不同的层次

字符集(Character Set):给字符编号
  问题:"中" 这个字符,用什么数字代表它?
  Unicode 的答案:'中' = U+4E2D(这个编号叫"码点 code point")
  → 字符集只管"字符 ↔ 编号"的映射,不管怎么存

编码(Encoding):把编号变成字节
  问题:U+4E2D 这个编号,实际存成哪些字节?
  UTF-8 的答案:3 个字节 E4 B8 AD
  UTF-16 的答案:2 个字节 4E 2D
  → 编码只管"编号 ↔ 字节"的转换

关键区别:Unicode 是「字符集」(字符→编号),UTF-8/UTF-16 是「编码」(编号→字节)。Unicode 只回答「这个字符是几号」,UTF-8/16 回答「这个号码怎么存成字节」。很多人把「Unicode」和「UTF-8」混为一谈——其实 Unicode 是一套「字符编号表」,UTF-8/16/32 是这套编号的不同「存储方案」。理解「字符集给编号、编码把编号变字节」这两层,就理解了后面所有问题——乱码是编码层的错、char 大小是编码层的选择。

二、为什么 char 是 2 字节:UTF-16

Java 的 char 固定 2 字节(16 位),这是因为 Java 内部用 UTF-16 表示字符

Java 诞生时(1995),Unicode 只有 65536 个字符(U+0000~U+FFFF,2 字节够用)
  → Java 就设计 char = 2 字节 = 一个 UTF-16 码元,能存一个字符
  → 当时"一个 char = 一个字符"是成立的

后来 Unicode 扩展到 100 多万个字符(增补字符,码点 > U+FFFF)
  → 2 字节的 char 存不下了!
  → 这些字符用"代理对"(2 个 char)表示(下面详述)
  → 于是"一个 char = 一个字符"不再总成立

所以 char 是 2 字节是「历史设计 + UTF-16」的结果——Java 早期假设「2 字节能存所有字符」,char 就定成 2 字节。这个假设在 Unicode 扩展后失效了(emoji 等存不下),但 char 的大小已经无法改(兼容性),只能用「代理对」打补丁。理解「char = 2 字节是因为 Java 内部用 UTF-16、早期够用」,就理解了这个大小的来历,也为「emoji 的坑」埋下了伏笔。

三、UTF-8 vs UTF-16 vs UTF-32:编码方案对比

同一套 Unicode 编号,有多种编码方案,各有取舍:

编码字节数特点适用
UTF-8变长 1~4 字节ASCII 兼容(英文 1 字节)、无字节序问题网络/文件传输主流
UTF-162 或 4 字节常用字符 2 字节、有字节序(BOM)Java/Windows 内部
UTF-32定长 4 字节定长好处理但费空间很少用
UTF-8 变长的巧妙:
  ASCII 字符(英文、数字)→ 1 字节(和 ASCII 完全兼容,省空间)
  中文等常用字符 → 3 字节
  emoji 等增补字符 → 4 字节
  → 英文为主的场景省空间、且兼容老的 ASCII 系统

为什么网络/文件用 UTF-8:
  ① ASCII 兼容(老系统能认)② 无字节序问题(UTF-16 有大小端 BOM 麻烦)
  ③ 省空间(英文场景)→ 成了事实标准

选型:UTF-8 是网络传输、文件存储的主流(兼容、无字节序、省空间);UTF-16 是 Java/Windows 的内部表示(Java 字符串内部用它)。UTF-32 定长但费空间、很少用。理解「UTF-8 变长兼容 ASCII 是网络/文件主流、UTF-16 是 Java 内部」,就知道该用哪个——对外统一 UTF-8,Java 内部是 UTF-16 你不用管。

四、乱码的根本原因:编解码不一致

乱码是字符处理最常见的问题,它的根本原因只有一个——编码和解码用了不同的字符编码

字符串在计算机里经历:"字符" ⇄ "字节" 的转换:
  编码(encode):字符 → 字节(存文件、发网络时)
  解码(decode):字节 → 字符(读文件、收网络时)

乱码 = 编码用 A、解码用 B(A ≠ B):
  "中文" --UTF-8 编码--> 字节 [E4 B8 AD E6 96 87]
         --GBK 解码--> 用 GBK 规则去解释这些字节 → 解释成别的字符 → 乱码

正确 = 编码解码用同一个字符集:
  "中文" --UTF-8 编码--> 字节 --UTF-8 解码--> "中文" ✓

关键认知:乱码不是「字节坏了」,而是「用错了解码规则」——同一串字节,用不同编码规则解读会得到不同字符。所以避免乱码的唯一办法是「全链路统一编码」:文件用什么编码存、就用什么编码读;网络请求声明什么编码、就用什么编码解。常见乱码场景:① 文件读写没指定编码(用了平台默认编码,不同机器不一样);② HTTP 请求响应编码不一致(Content-Type 的 charset);③ 数据库连接编码不匹配。解决方案统一是「处处显式指定 UTF-8」。理解「乱码是编解码不一致、解决靠全链路统一 UTF-8」,就掌握了乱码问题的根治方法。

五、代理对:emoji 和生僻字的坑

char 只有 2 字节,存不下码点 > U+FFFF 的「增补字符」(emoji、生僻字),Java 用 代理对(surrogate pair) 解决——用 2 个 char 表示一个增补字符

String emoji = "😀";   // U+1F600,超出 U+FFFF
emoji.length();                        // 2!——用了 2 个 char(一个代理对)
emoji.charAt(0);                       // 高代理项(无意义的半个字符)
emoji.codePointCount(0, emoji.length()); // 1——按码点算才是 1 个字符

// 遍历 emoji 字符串要按码点,不能按 char
emoji.codePoints().forEach(cp -> ...); // 正确按码点遍历

代理对机制:UTF-16 用「一对特殊的 char(高代理 + 低代理)」组合表示一个增补字符——增补字符占 2 个 char。这带来一系列坑:length() 返回 char 数不是字符数(emoji 算 2);charAt(i) 可能取到半个代理项(无意义);③ 截取字符串可能从代理对中间截断(得到无效字符);④ 反转字符串会破坏代理对。所以处理「可能含 emoji/生僻字」的字符串时,要用码点相关的 APIcodePointCountcodePoints()offsetByCodePoints)而非 char 相关的(lengthcharAt)。理解「代理对用 2 个 char 表示增补字符、导致 length ≠ 字符数」,就避开了 emoji 处理的坑(现在 emoji 无处不在,这个坑越来越常见)。

六、工程实践:统一编码

字符编码问题的工程解法就一句话——全链路统一 UTF-8、处处显式指定

文件读写:显式指定编码,别用平台默认
  new InputStreamReader(in, StandardCharsets.UTF_8)   // ✓ 显式 UTF-8
  new FileReader(file)                                 // ✗ 用平台默认编码,隐患

字符串 ⇄ 字节:显式指定编码
  str.getBytes(StandardCharsets.UTF_8)                 // ✓
  str.getBytes()                                       // ✗ 平台默认

HTTP:Content-Type 声明 charset=UTF-8
  数据库:连接串和表都用 utf8mb4(MySQL 的 utf8mb4 才能存 emoji!)
  JVM:-Dfile.encoding=UTF-8 兜底

核心原则:① 处处显式指定 UTF-8(别依赖「平台默认编码」——不同操作系统默认不同,是乱码的常见根源);② 用 StandardCharsets.UTF_8 常量(避免拼错编码名字符串);③ MySQL 存 emoji 要用 utf8mb4(MySQL 的 utf8 其实是 3 字节、存不下 4 字节的 emoji,utf8mb4 才是真正的 UTF-8)。这些实践的共同点是「消除编码的不确定性」——把「用哪个编码」明确定死为 UTF-8,全链路一致,就不会乱码。理解「工程上处处显式 UTF-8、注意 MySQL 的 utf8mb4」,就能在真实项目里根治编码问题。

记忆钩子:「char 是 2 字节(Java 内部 UTF-16);Unicode 是字符集(给字符编号码点)、UTF-8/16 是编码(编号→字节,UTF-8 变长兼容 ASCII 是网络主流);乱码=编解码用了不同编码,解决靠全链路统一 UTF-8 显式指定;char 存不下 emoji(>U+FFFF),用 2 个 char 代理对表示,导致 length ≠ 字符数,要用 codePoint API;MySQL 存 emoji 用 utf8mb4」

七、常见误区与追问

  • 误区:Unicode 就是 UTF-8。 不是——Unicode 是字符集(给字符分配编号/码点),UTF-8 是编码(把码点转成字节);Unicode 是「编号表」、UTF-8/16/32 是它的不同「存储方案」。
  • 误区:一个 char 就是一个字符。 不总是——char 是 2 字节的 UTF-16 码元,emoji 等增补字符(码点 > U+FFFF)要用 2 个 char(代理对)表示,此时一个字符占 2 个 char。
  • 误区:String.length() 返回字符个数。 返回 char 个数(UTF-16 码元数)——含 emoji 时一个字符算 2,要数字符个数用 codePointCount()。
  • 误区:乱码是字节损坏了。 不是——字节没坏,是「解码用了和编码不同的字符集」(如 UTF-8 编码用 GBK 解码),同一串字节用不同规则解读得到不同字符。
  • 追问:为什么 Java 的 char 是 2 字节? Java 内部用 UTF-16,char = 一个 UTF-16 码元 = 2 字节;早期 Unicode 只有 65536 个字符(2 字节够),后来扩展了但 char 大小无法改,增补字符改用代理对。
  • 追问:MySQL 存 emoji 为什么要用 utf8mb4 不用 utf8? MySQL 的 utf8 其实是最多 3 字节的阉割版,存不下 4 字节的 emoji(增补字符);utf8mb4 才是真正的 UTF-8(最多 4 字节),能存 emoji。
  • 追问:怎么正确处理含 emoji 的字符串? 用码点相关 API——codePointCount() 数字符、codePoints() 遍历、offsetByCodePoints() 定位,别用 length()/charAt()(会把代理对当 2 个 char,且可能取到半个代理项)。

八、加强记忆

Java 的 char2 字节(16 位),因为 Java 内部用 UTF-16 编码(char = 一个 UTF-16 码元)。要分清三层:Unicode 是「字符集」(给每个字符分配唯一编号/码点,如 ‘中’=U+4E2D)、UTF-8/UTF-16 是「编码」(把码点转成实际字节,UTF-8 变长 1~4 字节兼容 ASCII、是网络/文件主流,UTF-16 是 Java 内部)、字节流是实际存储。乱码的根本原因是「编码和解码用了不同的字符编码」(如 UTF-8 编码的字节用 GBK 解码),字节没坏、是解读规则错了,解决靠全链路统一 UTF-8、处处显式指定(别依赖平台默认编码、用 StandardCharsets.UTF_8)。一个大坑:char 只 2 字节,存不下 emoji 等增补字符(码点 > U+FFFF),它们用 2 个 char(代理对)表示,导致 length() 返回 char 数 ≠ 字符数(emoji 算 2)、charAt 可能取半个代理项、截取可能截断代理对——处理含 emoji 的字符串要用 码点 API(codePointCount/codePoints)。工程实践:处处显式 UTF-8、MySQL 存 emoji 用 utf8mb4(普通 utf8 是 3 字节存不下)。一句话「char 2 字节 UTF-16、Unicode 是字符集 UTF-8 是编码、乱码是编解码不一致靠统一 UTF-8、emoji 用代理对导致 length≠字符数、MySQL 用 utf8mb4」。