字符集、ASCII、Unicode 与 UTF-8 | JavaSE
字符集、ASCII、Unicode 与 UTF-8
一、学习目标
学完本章,你应该能够:
- 能够解释字符(Character)、字符集(Character Set)、码点(Code Point)、编码(Encoding)和字节(Byte)之间的基本关系。
- 能够解释为什么计算机最终必须把文字转换成数字和字节才能存储。
- 能够掌握 ASCII 的基本思想、范围和历史意义。
- 能够解释 GBK 的基本定位以及它与 ASCII 的兼容关系。
- 能够准确区分 Unicode 与 UTF-8,避免把二者当成同一个概念。
- 能够解释 UTF-8 为什么采用变长编码,以及 1~4 字节编码的大致规则。
- 能够说明为什么常见 ASCII 字符在 UTF-8 中仍只占一个字节。
- 能够理解 Java
char、Unicode 码点和 UTF-16 代码单元之间的基本关系。 - 为下一章“编码、解码与乱码问题”建立完整理论基础。
二、核心知识
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 知识问答
- 计算机为什么需要字符编码?
- 什么是字符集?
- 什么是 Unicode 码点?
U+0041表达的是什么?- ASCII 可以表示多少个代码位置?
- 标准 ASCII 本质需要多少位?
- 为什么通常仍然说 ASCII 字符占一个字节?
- GBK 的基本定位是什么?
- Unicode 出现是为了解决什么问题?
- Unicode 与 UTF-8 有什么区别?
- UTF-8 为什么属于变长编码?
- UTF-8 一个 Unicode 码点可能使用多少个字节?
- 为什么 UTF-8 兼容 ASCII?
- 常用汉字为什么通常需要三个 UTF-8 字节?
- 为什么不能说所有汉字一定三个字节?
- Java
char是多少位? - 一个 Unicode 码点是否永远等于一个 Java
char? String.length()为什么不一定等于 Unicode 码点数量?- 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);
}
}
不要运行,回答:
'A'转换成整数大约是多少?'我'转换出的数值表达的是什么层次的信息?- 得到数字以后是否意味着已经得到 UTF-8 字节?
- 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 的关系。 - [ ] 能区分内存中的字符表示与文件中的字节编码。