为什么要学这个?
你在 变量与数据类型 里已经知道 char 底层是数字,在 数据类型的转换 里用过 'A' + 1 = 66
但”为什么A是65?””UTF-8和Unicode什么关系?””为什么中文有时候会乱码?”——搞懂进制和编码,这些问题全部迎刃而解
零、先搞清楚:位、字节、字符
这三个概念是后面所有内容的地基,搞混了后面全白学
位(bit):最小的单位
计算机里最最底层的东西,就是一个开关:0 或 1
1 bit 只能表示两种状态(开/关、是/否、true/false)
你在 变量与数据类型 里学的 boolean,逻辑上就是 1 bit
字节(byte):8个bit打包在一起
1 字节 = 8 位(8 bit),这是计算机存储和传输的基本单位
1 字节能表示 2⁸ = 256 种不同的值(0255,或 -128127)
为什么是8个?历史原因,早期计算机设计者发现8位刚好够表示一个英文字符,就定下来了
比喻:bit 是一颗珠子(黑或白),byte 是把8颗珠子串成一串手链
1 | 1 bit: 0 或 1(一颗珠子) |
字符(character):人类能看懂的一个符号
一个字母 A、一个汉字 中、一个emoji 😀,每个都是一个字符
字符是给人看的,但计算机只认数字(字节),所以需要编码把字符变成字节
关键理解:一个字符 ≠ 一个字节
英文字母:1个字符 = 1个字节(在UTF-8下)
中文汉字:1个字符 = 3个字节(在UTF-8下)
emoji:1个字符 = 4个字节(在UTF-8下)
这就是为什么同样存10个字,中文文件比英文文件大
Java中的 byte vs Byte vs char vs String
| 写法 | 类型 | 说明 |
|---|---|---|
byte(小写) |
基本类型 | 就是1个字节,范围 -128~127,存原始数据用 |
Byte(大写) |
包装类 | byte 的对象版本,能放进集合、能为null |
char(小写) |
基本类型 | 1个字符,2字节(UTF-16编码单元) |
Character(大写) |
包装类 | char 的对象版本 |
String |
类 | 一串字符,底层是 char 数组 |
1 |
|
字节 vs 字符 在实际开发中的区别
1 |
|
什么时候用字节,什么时候用字符?
| 场景 | 用字节(byte) | 用字符(char/String) |
|---|---|---|
| 读写文本文件 | ✅ Reader/Writer | |
| 读写图片/视频/压缩包 | ✅ InputStream/OutputStream | |
| 网络传输 | ✅ 数据最终都是字节流 | |
| 显示给用户看 | ✅ 人看的是字符 | |
| 加密/哈希计算 | ✅ 操作的是原始字节 | |
| 数据库存文本 | ✅ String |
口诀:给人看的用字符,给机器传的用字节
一图总结关系
1 | bit(位) → 最小单位,0或1 |
一、进制:数数的不同方式
我们日常用十进制,纯粹是因为人有10根手指。计算机用二进制,因为电路只有通电/断电两种状态
四种常见进制
| 进制 | 基数 | 用到的数字 | Java前缀 | 举例 |
|---|---|---|---|---|
| 二进制 | 2 | 0, 1 | 0b |
0b1010 = 10 |
| 八进制 | 8 | 0-7 | 0 |
012 = 10 |
| 十进制 | 10 | 0-9 | 无 | 10 |
| 十六进制 | 16 | 0-9, A-F | 0x |
0xA = 10 |
为什么要有十六进制?
二进制太长了!比如数字255,二进制写成 11111111(8位),十六进制只要 FF(2位)
换算关系:1位十六进制 = 4位二进制,所以十六进制是二进制的”缩写”
怎么推导的?1位十六进制有16种可能(0~F),而 2⁴ = 16,刚好需要4位二进制
1 | 十六进制 二进制 |
所以十六进制和二进制可以逐位直接互换,不需要计算
0xFF → 拆成 F 和 F → 1111 1111(8个1全满)
二进制 0010 1101 → 每4位一组 → 2D
颜色代码 #FF5733 就是十六进制:R=FF(255), G=57(87), B=33(51)
进制转换口诀
十进制 → 二进制:除2取余,倒着读
二进制 → 十进制:每一位 × 2的n次方,然后加起来
1 | 十进制 13 → 二进制: |
Java中玩进制
1 |
|
⚠️ 八进制的坑
int x = 012; 结果是10不是12!因为以 0 开头Java认为是八进制
这就是为什么写代码时数字前面不要随便补零
二、ASCII:最古老的编码表
历史背景
1960年代,美国人发明了计算机,需要让字符和数字对应起来
他们只考虑了英文,所以 ASCII 只有 128个字符(用7位二进制就够了)
ASCII表的核心记忆
| 字符 | 十进制 | 二进制 | 助记 |
|---|---|---|---|
0 |
48 | 0110000 | 数字从48开始 |
9 |
57 | 0111001 | |
A |
65 | 1000001 | 大写字母从65开始 |
Z |
90 | 1011010 | |
a |
97 | 1100001 | 小写字母从97开始 |
z |
122 | 1111010 | |
| 空格 | 32 | 0100000 | |
换行\n |
10 | 0001010 |
大写和小写差32:'a' - 'A' = 32,这就是为什么 数据类型的转换 里 lower - 32 能转大写
0-31 是控制字符(不可打印),32-126 是可打印字符
Java实操
1 |
|
ASCII的局限
只有128个位置,放完英文字母、数字、标点就满了
中文有几万个字,日文、韩文、阿拉伯文……ASCII根本装不下
这就是为什么需要更大的编码方案 → Unicode
三、Unicode:给全世界每个字符一个编号
一句话理解
ASCII 是”英文字符编号表”,Unicode 是”全人类字符编号表”
Unicode 给每个字符分配了一个唯一编号,叫做 码点(Code Point)
写法:U+ 加十六进制数,比如 U+0041 就是字母 A,U+4F60 就是”你”
Unicode 和 ASCII 的关系
Unicode 完全兼容 ASCII:前128个编号和ASCII一模一样
A 在 ASCII 里是 65,在 Unicode 里是 U+0041(41的十六进制 = 65的十进制)
所以 ASCII 是 Unicode 的子集,学 Unicode 不会白费之前学的 ASCII
Unicode 有多大?
目前收录了超过 14万个字符,覆盖 150+ 种语言
范围从 U+0000 到 U+10FFFF,分成 17 个平面(Plane)
| 平面 | 范围 | 说明 |
|---|---|---|
| 基本平面 BMP | U+0000 ~ U+FFFF |
日常用到的99%的字符都在这里 |
| 补充平面 | U+10000 ~ U+10FFFF |
emoji😀、古文字、生僻字、数学符号等 |
Java中的Unicode
Java的 char 是 2字节(16位),刚好能装下BMP(基本平面)的所有字符
但emoji等补充平面的字符超过了16位,一个 char 装不下,需要两个char(代理对 Surrogate Pair)
1 |
|
重点理解
Unicode 只是一张编号表——规定”你”是4F60号
但怎么存到硬盘/怎么在网络上传输,Unicode自己不管
这就引出了下一个问题:编码方式(UTF-8, UTF-16, UTF-32)
四、UTF-8 / UTF-16 / UTF-32:Unicode的存储方案
核心比喻
Unicode 是”字典”——规定了每个字的编号
UTF-8/UTF-16/UTF-32 是”快递包装方式”——规定怎么把编号打包成字节存储和传输
同一个字(同一个Unicode码点),用不同的UTF方案包装出来的字节是不一样的
UTF-32:最傻最简单
每个字符固定占 4字节(32位)
优点:简单直接,编号是多少就存多少
缺点:太浪费空间!一个英文字母 A 本来1字节就够,硬要占4字节
几乎没人用,了解即可
UTF-16:Java内部用的方案
BMP字符(U+0000~`U+FFFF`)占 2字节
补充平面字符(emoji等)占 4字节(用代理对)
Java的 char 就是 UTF-16 编码单元,这就是为什么 char 是2字节
对中文友好(中文都在BMP里,每个字固定2字节),对英文不友好(英文也要2字节)
UTF-8:互联网的王者 ⭐
变长编码:不同字符占不同字节数,既省空间又兼容ASCII
| Unicode范围 | UTF-8字节数 | 覆盖内容 |
|---|---|---|
U+0000 ~ U+007F |
1字节 | ASCII(英文、数字、标点) |
U+0080 ~ U+07FF |
2字节 | 拉丁文扩展、希腊文、阿拉伯文等 |
U+0800 ~ U+FFFF |
3字节 | 中日韩文字、大部分常用字符 |
U+10000 ~ U+10FFFF |
4字节 | emoji、古文字、生僻字 |
为什么UTF-8是王者?
纯英文内容只要1字节/字符,和ASCII一样省空间
完全兼容ASCII:ASCII文件不用改一个字节就是合法的UTF-8文件
全球互联网 97%+ 的网页用UTF-8
同一个字的不同包装对比
1 | 字符"A"(U+0041): |
Java实操:亲眼看字节
1 |
|
五、乱码是怎么产生的?
一句话:用编码A写,用编码B读 → 乱码
就像你用中文写了封信,收件人用日语词典查 → 看到的全是乱七八糟的字
经典乱码场景
| 场景 | 原因 | 解决办法 |
|---|---|---|
| 网页显示乱码 | HTML没声明charset,浏览器猜错了 | 加 <meta charset="UTF-8"> |
| 读文件乱码 | 文件是GBK编码,代码用UTF-8读 | 指定正确的编码读取 |
| 数据库乱码 | 数据库、连接、代码三方编码不统一 | 全部统一成UTF-8 |
| 控制台乱码 | IDE控制台编码和程序输出编码不一致 | IDEA设置里改File Encoding |
Java中读写文件指定编码
1 |
|
防乱码黄金法则:全链路统一UTF-8
Java源文件 → UTF-8
IDEA File Encoding → 全部 UTF-8
数据库建库 → CHARACTER SET utf8mb4(注意是utf8mb4不是utf8)
HTTP响应头 → Content-Type: text/html; charset=UTF-8
六、GBK / GB2312:中文编码的历史包袱
为什么有GBK?
早年Unicode还没普及,中国自己搞了一套中文编码
GB2312(1980)→ GBK(1995)→ GB18030(2000),越来越大
GBK的特点
英文 1 字节(兼容ASCII),中文 2 字节
只收录了中文(及少量其他字符),不包含日文假名、emoji等
GBK vs UTF-8 存中文
GBK:每个中文 2字节
UTF-8:每个中文 3字节
所以存纯中文时GBK更省空间,但现在存储不值钱了,统一用UTF-8更省心
你还会在哪里遇到GBK?
Windows中国区系统默认编码是GBK(这就是为什么Windows CMD经常乱码)
一些老的Java项目、老数据库
遇到了就指定编码读取:new InputStreamReader(stream, "GBK")
七、总结:它们之间的关系
1 | ┌──────────────────────────────────────────────────────┐ |
记住这几句话就够了
ASCII 是老祖宗,只管英文,128个字符
Unicode 是全球统一的字符编号表,兼容ASCII
UTF-8 是 Unicode 的存储方案,变长省空间,互联网标准,无脑选它
GBK 是中国的老编码,遇到老系统才需要处理
Java的char 用的是 UTF-16,所以是2字节,emoji需要两个char
乱码 = 编码和解码用了不同的方案,统一UTF-8就能根治
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 数据类型的转换 | java基础 | 运算符 |