字符集、ASCII、Unicode 与 UTF-8 | JavaSE

字符集、ASCII、Unicode 与 UTF-8

一、学习目标

学完本章,你应该能够:

  1. 能够解释字符(Character)、字符集(Character Set)、码点(Code Point)、编码(Encoding)和字节(Byte)之间的基本关系。
  2. 能够解释为什么计算机最终必须把文字转换成数字和字节才能存储。
  3. 能够掌握 ASCII 的基本思想、范围和历史意义。
  4. 能够解释 GBK 的基本定位以及它与 ASCII 的兼容关系。
  5. 能够准确区分 Unicode 与 UTF-8,避免把二者当成同一个概念。
  6. 能够解释 UTF-8 为什么采用变长编码,以及 1~4 字节编码的大致规则。
  7. 能够说明为什么常见 ASCII 字符在 UTF-8 中仍只占一个字节。
  8. 能够理解 Java char、Unicode 码点和 UTF-16 代码单元之间的基本关系。
  9. 为下一章“编码、解码与乱码问题”建立完整理论基础。

二、核心知识

2.1 计算机本质上只存储二进制

我们看到:

A
中
我
Java
星雨笔录
😀

但是计算机底层最终保存的是:

0
1

例如:

01001010
01100001
01110110
01100001

因此计算机必须建立一套规则:

字符
  ↓
某个数字
  ↓
按照某种规则编码
  ↓
字节序列
  ↓
二进制

反过来:

字节序列
  ↓
按照某种规则解释
  ↓
数字
  ↓
字符

这就是字符编码体系的根本问题:

人类使用文字,而计算机保存数字和字节,二者之间必须建立稳定映射。


2.2 什么是字符

字符(Character)可以简单理解为:

文本系统中的一个抽象符号。

例如:

A
a
1
我
中
@
€

都是字符。

但需要注意:

“用户眼睛看到的一个图形”与“计算机中的一个 Unicode 码点”并不永远一一对应。

例如某些:

组合字符
Emoji 序列

可能由多个 Unicode 码点共同组成一个用户感知字符。

JavaSE 当前阶段先理解:

字符
=
需要被计算机表示的文本符号

即可。


2.3 什么是字符集

字符集(Character Set)可以先理解为:

一套字符以及这些字符对应编号的规则体系。

最经典的例子:

ASCII

规定:

'A' → 65
'a' → 97
'0' → 48

于是:

字符
↓
编号

之间建立起确定关系。

不同历史时期、国家和系统出现过很多字符集与编码方案,例如:

ASCII
GBK
Unicode

它们的出现,本质都在解决:

怎样让计算机表示人类文字。


2.4 为什么字符集会越来越复杂

最早计算机主要服务英语环境。

只需要表示:

英文字母
数字
标点符号
控制字符

字符数量很少。

于是:

ASCII

就基本够用。

后来计算机进入:

中国
日本
韩国
欧洲
中东
世界各国

问题来了:

中文怎么办?
日文怎么办?
阿拉伯文怎么办?
更多数学符号怎么办?
Emoji 怎么办?

于是不同国家和地区发展出各种编码体系。

但大量区域标准同时存在又带来了:

编码不统一
文件跨系统乱码
同一个数字在不同编码中含义不同
国际软件开发困难

最终需要:

一个能够统一表示世界文字和符号的体系。

这就是 Unicode 出现的重要背景。


2.5 ASCII

ASCII:

American Standard Code for Information Interchange

中文通常称为:

美国信息交换标准代码。

标准 ASCII 使用:

7 bit

因此可以表示:

2^7 = 128

个代码值。

范围:

0 ~ 127

例如:

| 字符 | ASCII 十进制值 | | ---- | -------------: | | 0 | 48 | | A | 65 | | a | 97 |

因此以前学习:

char ch = 'A';

System.out.println(ch + 1);

出现数值运算时,我们已经接触过:

字符
↔
数字编号

这种思想。


2.6 为什么经常说 ASCII 占一个字节

ASCII 本身只需要:

7 位

即可表示 128 个值。

但现代计算机通常按:

byte

处理存储单位。

一个字节:

8 bit

因此标准 ASCII 字符通常表示成:

0xxxxxxx

最高位为:

0

剩下 7 位表示 ASCII 值。

例如:

'A'

ASCII:

65

二进制:

01000001

所以教学中经常说:

ASCII 字符占一个字节。

更准确地说:

ASCII 是 7 位编码体系,实际通常使用一个 8 位字节承载。


2.7 ASCII 的局限

ASCII 最大的问题非常明显:

只有 128 个代码位置

显然不能表示:

我
你
中
国
日文
韩文
阿拉伯文
Emoji

因此 ASCII 不可能承担:

全球文本统一表示

的任务。

但它留下了非常重要的历史基础:

A-Z
a-z
0-9
基础符号

大量后来编码都尽可能兼容它。


2.8 GBK

为了表示大量中文字符,中国出现过多种中文编码标准。

Java 学习中经常接触:

GBK

可以简单理解为:

一种面向中文环境的传统字符编码。

GBK 中通常:

ASCII 范围字符
→ 1 字节

大量中文字符
→ 2 字节

并且:

GBK 兼容 ASCII 的基本编码。

例如:

A

在 ASCII 与 GBK 中可以保持相同的基础字节表示。

这也是为什么:

英文和数字

在很多编码错误中可能仍显示正常,而中文已经乱码。

但 GBK 的设计目标主要面向中文环境:

它不是全球统一文本编码方案

现代跨平台开发一般更推荐:

Unicode
+
UTF-8

体系。


2.9 Unicode 到底是什么

Unicode 的核心目标是:

为全球各种文字、符号建立统一编号体系。

例如:

A

对应 Unicode 码点:

U+0041

字符:

对应:

U+6211

这里:

U+6211

不是说:

文件里一定直接存储 62 11

而是在表达:

Unicode 给字符“我”分配的唯一编号。

这个编号称为:

码点(Code Point)


2.10 什么是码点

Unicode 中每一个编码位置都有一个编号。

标准写法:

U+XXXX

例如:

A
→ U+0041

a
→ U+0061

我
→ U+6211

因此可以建立:

字符
↓
Unicode
↓
码点

例如:

"我"
↓
U+6211

码点解决的是:

这个字符在 Unicode 世界里的编号是什么?


2.11 Unicode 不是简单的“每个字符两个字节”

这是一个非常重要的历史误区。

不能再理解成:

Unicode
=
每字符固定 2 字节

现代 Unicode 的代码空间范围是:

U+0000
~
U+10FFFF

也就是说 Unicode 已经远远超过:

16 bit

能够直接表达的范围。

因此:

Unicode

应该理解成:

全球字符的统一编码标准与码点体系。

而真正把这些码点转换为:

字节

还需要:

编码形式(Encoding Form)

例如:

UTF-8
UTF-16
UTF-32

2.12 Unicode 与 UTF-8 不是一个概念

这是本章最重要的知识点之一。

很多初学者会说:

UTF-8 就是 Unicode

不准确。

可以这样理解:

Unicode
负责:
这个字符编号是多少?

例如:

我
↓
U+6211

而:

UTF-8
负责:
U+6211 应该怎样转换成字节序列?

因此:

Unicode
=
字符编号体系

UTF-8
=
Unicode 的一种编码形式

类比:

Unicode
像身份证号码制度

UTF-8
像把身份证号码按照某种规则
转换成可以传输和保存的字节格式

2.13 UTF 是什么

UTF:

Unicode Transformation Format

即:

Unicode 转换格式。

Unicode 常见编码形式包括:

UTF-8
UTF-16
UTF-32

它们都能够表示 Unicode 的完整字符范围。

区别主要在于:

码点如何转换成代码单元
需要多少存储空间
字节组织方式

2.14 UTF-8

UTF-8 是:

Unicode 的一种可变长度、面向字节的编码形式。

所谓:

可变长度

是指:

不同 Unicode 码点可能使用不同数量的字节。

UTF-8 使用:

1 ~ 4 字节

表示 Unicode 码点。


2.15 UTF-8 的四种基本长度

可以建立下面这张核心表:

| Unicode 范围 | UTF-8 字节数 | 基本格式 | | -------------------- | -----------: | ------------------------------------- | | U+0000 ~ U+007F | 1 | 0xxxxxxx | | U+0080 ~ U+07FF | 2 | 110xxxxx 10xxxxxx | | U+0800 ~ U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx | | U+10000 ~ U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |

这里暂时不要求:

手工背诵每一位转换算法。

真正需要理解的是:

UTF-8
会根据 Unicode 码点范围
决定使用 1、2、3 或 4 个字节。

2.16 为什么 UTF-8 要设计成变长

如果所有字符都固定使用:

4 字节

虽然规则简单,但大量英文文本会浪费空间。

因为:

A
B
C
1
2
3

这些基础字符只需要很小的编号。

UTF-8 于是设计为:

小范围码点
→ 少量字节

大范围码点
→ 更多字节

例如 ASCII 范围:

U+0000 ~ U+007F

只需要:

1 byte

因此英文文本非常紧凑。


2.17 UTF-8 为什么兼容 ASCII

字符:

A

ASCII:

01000001

UTF-8 对:

U+0000 ~ U+007F

使用格式:

0xxxxxxx

因此:

A

在 UTF-8 中仍然是:

01000001

所以:

标准 ASCII 字节序列本身也是合法 UTF-8。

这就是 UTF-8:

向后兼容 ASCII

的重要意义。


2.18 常用汉字为什么通常是 3 字节

例如:

码点:

U+6211

它位于:

U+0800 ~ U+FFFF

因此 UTF-8 使用:

3 字节

编码。

所以在 Java 学习中常见结论:

普通英文字符
→ UTF-8 通常 1 字节

常用汉字
→ UTF-8 通常 3 字节

这个结论对于绝大多数常见中文文本非常实用。

但不能升级成:

所有汉字永远都是 3 字节。

Unicode 中还存在:

U+10000 以上

的补充平面汉字。

这些码点在 UTF-8 中需要:

4 字节

因此准确口径是:

绝大多数日常使用、位于基本多文种平面中的汉字在 UTF-8 中占 3 字节;补充平面的部分汉字需要 4 字节。


2.19 Emoji 为什么经常需要 4 字节

很多 Emoji 的 Unicode 码点位于:

U+10000

以上。

例如某些 Emoji 属于:

补充平面

因此一个对应码点使用 UTF-8 编码时通常需要:

4 byte

但还要注意:

用户眼中的“一个 Emoji”有时可能由多个 Unicode 码点组合。

所以:

一个图形
=
一定一个码点
=
一定四个字节

也不是永远成立。


2.20 字符、码点与字节必须分层理解

这是整个字符编码体系的核心模型:

人看到的字符
      ↓
Unicode 码点
      ↓
选择编码形式
例如 UTF-8
      ↓
字节序列
      ↓
文件 / 网络 / 磁盘

例如:

我
↓
U+6211
↓
UTF-8
↓
3 个字节

因此:

字符
码点
字节

不是同一个层次。


三、使用方法

3.1 先判断问题属于哪个层次

以后遇到字符问题,可以依次问:

第一层:
我讨论的是字符本身吗?

第二层:
我讨论的是 Unicode 码点吗?

第三层:
我讨论的是 UTF-8 / GBK 等编码吗?

第四层:
我讨论的是文件中的具体字节吗?

如果层次混在一起,就非常容易产生错误认知。


3.2 判断 ASCII 范围

例如:

'A'

Unicode:

U+0041

属于:

U+0000 ~ U+007F

因此 UTF-8:

1 byte

3.3 判断普通汉字

例如:

'我'

Unicode:

U+6211

属于:

U+0800 ~ U+FFFF

因此 UTF-8:

通常为 3 byte

3.4 Unicode 码点写法

看到:

U+0041

应该读成:

Unicode 码点 U+0041。

而不是:

“UTF-8 编码 0041”。

因为:

U+0041

表达的是:

码点

真正 UTF-8 字节序列属于:

编码结果

是下一层概念。


3.5 建立编码体系比较表

| 名称 | 核心定位 | 特点 | | ------- | -------------------- | ---------------------------------- | | ASCII | 早期英文字符编码 | 7 bit,128 个代码位置 | | GBK | 传统中文编码 | ASCII 兼容,大量中文常用双字节表示 | | Unicode | 全球统一字符编码标准 | 为全球字符分配统一码点 | | UTF-8 | Unicode 编码形式 | 1~4 字节、可变长、ASCII 兼容 | | UTF-16 | Unicode 编码形式 | 使用 16 位代码单元 | | UTF-32 | Unicode 编码形式 | 使用 32 位代码单元 |

最容易混淆的是:

Unicode
≠
UTF-8

四、原理与进阶

4.1 Unicode 基本多文种平面 BMP

Unicode 码点范围:

U+0000
~
U+10FFFF

其中:

U+0000
~
U+FFFF

称为:

基本多文种平面(Basic Multilingual Plane, BMP)

大量日常文字都位于 BMP,例如:

拉丁字母
大量汉字
常见符号

而:

U+10000
~
U+10FFFF

属于:

补充平面范围。


4.2 Java char 与 Unicode

Java:

char

是:

16 bit

也就是:

2 byte

这并不意味着:

一个 Java char 永远可以表示一个完整 Unicode 字符。

对于 BMP 中大量码点:

一个 char

可以表示一个 UTF-16 代码单元。

但 Unicode 补充字符:

U+10000 以上

需要两个 char 组成:

代理对(Surrogate Pair)

因此:

Unicode 码点

与:

Java char

不能永远认为:

1 : 1

4.3 为什么 String.length() 不一定等于“字符数量”

Java String 的很多基础操作以:

char / UTF-16 代码单元

为单位。

如果字符串只包含:

普通英文
常见中文

往往会感觉:

length()
=
字符数

但遇到补充字符:

一个 Unicode 码点

可能需要:

两个 char

因此:

string.length()

严格来说统计的是:

UTF-16 char 代码单元数量。

不一定等于:

Unicode 码点数量

更不一定等于:

用户视觉上的字符数量

这是 Unicode 编程中非常重要的边界。


4.4 Java 内存字符与文件编码是两个层次

一定不要形成:

Java String 是 UTF-8

这样的错误结论。

可以建立:

Java String
      │
      │ 程序内部处理字符
      ▼
Unicode / UTF-16 代码单元语义
      │
      │ 编码
      ▼
byte[]
      │
      ├── UTF-8
      ├── GBK
      ├── UTF-16
      └── 其他字符集

所以:

字符串在 Java 内部怎样表示,与字符串写入文件时采用什么字节编码,是两个不同问题。

下一章会正式把:

String
↔
byte[]

连接起来。


4.5 为什么 UTF-8 没有大小端问题

UTF-8 的基本单位就是:

byte

按字节序列依次排列。

不像:

UTF-16
UTF-32

使用多字节代码单元时需要讨论:

Big Endian
Little Endian

因此 UTF-8 本身没有:

大端 UTF-8
小端 UTF-8

这样的两套字节顺序。


4.6 BOM 是什么

有些文本文件开头可能出现:

BOM(Byte Order Mark,字节顺序标记)

UTF-16 / UTF-32 中,BOM 可以帮助判断:

字节序

UTF-8 不存在字节序问题,因此 UTF-8 中 BOM:

不是为了判断大小端

它有时被当作:

UTF-8 文件标记

但不是所有协议和程序都希望 UTF-8 文件带 BOM。

JavaSE 当前阶段只需要认识这个概念,不需要深入处理。


五、实践应用

5.1 为什么网页普遍使用 UTF-8

一个现代网站可能同时出现:

中文
English
日本語
Emoji
数学符号

如果分别使用地区字符集:

系统复杂度会非常高

UTF-8:

Unicode 全字符范围
+
ASCII 兼容
+
字节序列适合文件与网络传输

因此成为现代 Web 最重要的文本编码之一。


5.2 为什么 Java 项目应该统一编码

假设:

Java 源文件
IDE
Maven
数据库
HTTP
前端
Linux 服务器
日志

各自使用不同编码:

UTF-8
GBK
UTF-16
……

非常容易出现:

本地正常
服务器乱码
数据库乱码
日志乱码
接口乱码

所以现代项目通常会尽量:

统一 UTF-8。

真正的编码配置与乱码排查将在下一章展开。


5.3 星雨笔录中的字符编码

例如博客正文:

Java集合框架详解😀

从业务角度是:

String

但是:

保存 Markdown
HTTP 返回
数据库存储
浏览器解析

最终都必须经历:

字符
↔
字节

如果各环节编码协议不一致:

星雨笔录

就有可能变成:

星雨...

因此字符编码绝不是:

“只是 IO 流里面一个小知识点”。

它实际上贯穿:

文件
数据库
网络
Web
操作系统
编译器
IDE

整个软件系统。


六、常见问题

6.1 ASCII 是 7 位还是 8 位?

标准 ASCII:

7 bit

表示:

128 个值

实际计算机通常用:

1 byte = 8 bit

承载,所以经常写成:

0xxxxxxx

不要把:

ASCII 本质 7 位

和:

实际通常占一个字节

混淆。


6.2 Unicode 和 UTF-8 是不是一个东西?

不是。

Unicode
→ 统一字符与码点体系

UTF-8
→ Unicode 的一种编码形式

这是本章必须掌握的核心区别。


6.3 Unicode 是固定两个字节吗?

不是。

现代 Unicode 代码空间:

U+0000 ~ U+10FFFF

真正存储时需要:

UTF-8
UTF-16
UTF-32

等编码形式。

不能再使用:

Unicode = 2 字节

这种过度简化。


6.4 UTF-8 中英文一定占一个字节吗?

对于:

标准 ASCII 范围字符

是一个字节。

例如:

A
a
0

都在:

U+0000 ~ U+007F

因此 UTF-8 为一个字节。

但“英文文本”如果包含:

特殊 Unicode 符号
扩展字符

不能简单全部概括成 ASCII。


6.5 UTF-8 中中文一定三个字节吗?

不能说“所有中文一定三个字节”。

更准确:

绝大多数日常常用汉字
位于 BMP
→ UTF-8 通常 3 字节

但 Unicode 补充平面的部分汉字:

→ UTF-8 需要 4 字节

6.6 Java char 就等于一个 Unicode 字符吗?

不能绝对这样说。

char
=
16 位 UTF-16 代码单元

BMP 中大量字符:

一个 char

即可表示。

补充字符:

需要两个 char

组成代理对。


6.7 UTF-8 为什么没有大小端?

因为 UTF-8 是:

面向字节

的编码形式。

不存在一个代码单元内部需要重新排列多个字节的问题。


6.8 为什么英文经常不乱码,而中文乱码?

一个重要原因是:

ASCII 范围

被 UTF-8、GBK 等多种常见编码兼容。

因此:

abc123

即使编码环境出现问题,也可能碰巧仍然正确。

而中文在:

UTF-8
GBK

中的字节表示完全不同,因此更容易暴露乱码。

具体乱码机制下一章正式解释。


七、练习与验收

7.1 知识问答

  1. 计算机为什么需要字符编码?
  2. 什么是字符集?
  3. 什么是 Unicode 码点?
  4. U+0041 表达的是什么?
  5. ASCII 可以表示多少个代码位置?
  6. 标准 ASCII 本质需要多少位?
  7. 为什么通常仍然说 ASCII 字符占一个字节?
  8. GBK 的基本定位是什么?
  9. Unicode 出现是为了解决什么问题?
  10. Unicode 与 UTF-8 有什么区别?
  11. UTF-8 为什么属于变长编码?
  12. UTF-8 一个 Unicode 码点可能使用多少个字节?
  13. 为什么 UTF-8 兼容 ASCII?
  14. 常用汉字为什么通常需要三个 UTF-8 字节?
  15. 为什么不能说所有汉字一定三个字节?
  16. Java char 是多少位?
  17. 一个 Unicode 码点是否永远等于一个 Java char
  18. String.length() 为什么不一定等于 Unicode 码点数量?
  19. Java 内部字符串与文件字节编码为什么属于两个层次?

7.2 代码阅读

观察:

public class UnicodeDemo {

    public static void main(String[] args) {

        char a = 'A';
        char chinese = '我';

        System.out.println((int) a);
        System.out.println((int) chinese);
    }
}

不要运行,回答:

  1. 'A' 转换成整数大约是多少?
  2. '我' 转换出的数值表达的是什么层次的信息?
  3. 得到数字以后是否意味着已经得到 UTF-8 字节?
  4. Unicode 码点和 UTF-8 字节为什么不是一个概念?

7.3 手写代码

任务一:ASCII 验证

输出:

'A'
'a'
'0'

对应的数值。

然后验证:

65
97
48

任务二:Unicode 码点

选择:

A
我
中

查出并记录:

Unicode 码点

写成:

U+XXXX

格式。


任务三:UTF-8 字节数判断

不使用 Java 编码 API,仅根据码点范围判断:

A
我

在 UTF-8 中分别应该使用几个字节。


7.4 Debug

判断下面说法是否正确,并修正错误:

1. ASCII 是 8 位编码,可以表示 256 个字符。

2. Unicode 就是 UTF-8。

3. Unicode 中每个字符固定占两个字节。

4. UTF-8 中所有字符固定占三个字节。

5. UTF-8 中所有中文都固定占三个字节。

6. 一个 Java char 永远等于一个 Unicode 字符。

7. Java String 本身就是 UTF-8 字节。

要求:

不只写“错”,必须说明错误发生在哪个概念层次。


7.5 综合训练

请画出:

字符
↓
Unicode 码点
↓
UTF-8
↓
字节
↓
文件

然后分别用:

A
我

填写每一层。

重点回答:

哪一步是在确定字符编号?

哪一步是在生成字节?

为什么 Unicode 和 UTF-8 不能合并成一个概念?

7.6 本章验收

如果你能够闭卷完整解释下面这条链路,本章即可认为掌握:

计算机最终保存的是字节。

ASCII 是早期英文环境中的字符编码体系,
使用 7 位表示 128 个代码值,
现代系统通常用一个字节承载。

GBK 是传统中文编码之一,
兼容 ASCII,
大量中文字符通常使用两个字节表示。

Unicode 的目标是统一表示世界文字和符号,
为字符分配统一的 Unicode 码点。

Unicode 本身不能简单理解成“文件中的固定两个字节”。

UTF-8 是 Unicode 的一种编码形式,
使用 1~4 个字节表示 Unicode 码点。

ASCII 范围在 UTF-8 中保持一个字节,
因此 UTF-8 兼容 ASCII。

绝大多数日常常用汉字在 UTF-8 中是三个字节,
但补充平面的部分汉字需要四个字节。

Java char 是 16 位 UTF-16 代码单元,
一个补充 Unicode 码点可能需要两个 char。

因此:
字符、
Unicode 码点、
Java char、
UTF-8 字节
必须分层理解。

最终:

  • [ ] 能解释字符集。
  • [ ] 能解释码点。
  • [ ] 能说出 ASCII 核心规则。
  • [ ] 能说明 GBK 基本特点。
  • [ ] 能准确区分 Unicode 与 UTF-8。
  • [ ] 能说出 UTF-8 的 1~4 字节结构。
  • [ ] 能解释 UTF-8 为什么兼容 ASCII。
  • [ ] 不再说“所有汉字 UTF-8 都一定 3 字节”。
  • [ ] 能解释 Java char 与 Unicode 的关系。
  • [ ] 能区分内存中的字符表示与文件中的字节编码。