进制与编码

为什么要学这个?

你在 变量与数据类型 里已经知道 char 底层是数字,在 数据类型的转换 里用过 'A' + 1 = 66

但”为什么A是65?””UTF-8和Unicode什么关系?””为什么中文有时候会乱码?”——搞懂进制和编码,这些问题全部迎刃而解

零、先搞清楚:位、字节、字符

这三个概念是后面所有内容的地基,搞混了后面全白学

位(bit):最小的单位

计算机里最最底层的东西,就是一个开关:01

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
2
3
4
5
6
7
8
1 bit:    0 或 1(一颗珠子)
1 byte: 01000001(8颗珠子串在一起)= 十进制 65 = 字符 'A'

常见单位换算:
1 Byte(B) = 8 bit
1 KB = 1024 Byte
1 MB = 1024 KB
1 GB = 1024 MB

字符(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testByteAndChar() {
// byte:存的是原始数字/字节,范围 -128~127
byte b = 65;
System.out.println("byte值:" + b); // 65
System.out.println("当字符看:" + (char) b); // A

// char:存的是字符,底层也是数字
char c = 'A';
System.out.println("char值:" + c); // A
System.out.println("当数字看:" + (int) c); // 65

// 它俩存的都是65,但"身份"不同:
// byte 说:"我是数字65"
// char 说:"我是字符A"

// Byte:byte的包装类,可以为null
Byte boxed = null; // ✅ 基本类型 byte 不能为null,但 Byte 可以
boxed = Byte.valueOf(b);
System.out.println("Byte对象:" + boxed); // 65
}

字节 vs 字符 在实际开发中的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testByteVsChar() throws Exception {
String text = "Hello你好";

// 字符层面:人看到7个字
System.out.println("字符数:" + text.length()); // 7

// 字节层面:计算机实际要存多少字节
byte[] utf8Bytes = text.getBytes("UTF-8");
System.out.println("UTF-8字节数:" + utf8Bytes.length); // 11(Hello=5 + 你好=6)

byte[] gbkBytes = text.getBytes("GBK");
System.out.println("GBK字节数:" + gbkBytes.length); // 9(Hello=5 + 你好=4)

// 所以同一段文字,用不同编码,字节数是不一样的!
// 字符数永远是7,但字节数取决于编码方案
}

什么时候用字节,什么时候用字符?

场景 用字节(byte) 用字符(char/String)
读写文本文件 ✅ Reader/Writer
读写图片/视频/压缩包 ✅ InputStream/OutputStream
网络传输 ✅ 数据最终都是字节流
显示给用户看 ✅ 人看的是字符
加密/哈希计算 ✅ 操作的是原始字节
数据库存文本 ✅ String

口诀:给人看的用字符,给机器传的用字节

一图总结关系

1
2
3
4
5
6
7
bit(位)          →  最小单位,01
8个bit
byte(字节) → 计算机存储/传输的基本单位
↓ 通过编码规则(UTF-8等)组合
character(字符) → 人看到的一个符号(A、中、😀)
↓ 多个字符串在一起
String(字符串) → "Hello你好"

一、进制:数数的不同方式

我们日常用十进制,纯粹是因为人有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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
十六进制  二进制
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
A 1010
B 1011
C 1100
D 1101
E 1110
F 1111

所以十六进制和二进制可以逐位直接互换,不需要计算

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
2
3
4
5
6
7
8
9
10
11
十进制 13 → 二进制:
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
倒着读 → 1101

二进制 1101 → 十进制:
1×2³ + 1×2² + 0×2¹ + 1×2⁰
= 8 + 4 + 0 + 1
= 13

Java中玩进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testRadix() {
// Java中直接写不同进制的数字
int bin = 0b1101; // 二进制 → 13
int oct = 015; // 八进制 → 13
int dec = 13; // 十进制 → 13
int hex = 0xD; // 十六进制 → 13

// 它们本质上是同一个数
System.out.println(bin == oct && oct == dec && dec == hex); // true

// 十进制 → 其他进制(转成字符串)
System.out.println(Integer.toBinaryString(255)); // "11111111"
System.out.println(Integer.toOctalString(255)); // "377"
System.out.println(Integer.toHexString(255)); // "ff"

// 其他进制字符串 → 十进制
System.out.println(Integer.parseInt("1101", 2)); // 13(二进制解析)
System.out.println(Integer.parseInt("ff", 16)); // 255(十六进制解析)
}

⚠️ 八进制的坑

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testAscii() {
// char 和 int 可以直接互转
char ch = 'A';
int code = ch;
System.out.println("A 的 ASCII 码:" + code); // 65

// 打印整张 ASCII 可见字符表
System.out.println("=== ASCII 可见字符表 ===");
for (int i = 32; i <= 126; i++) {
System.out.printf("%3d = '%c' ", i, (char) i);
if ((i - 31) % 8 == 0) System.out.println(); // 每行8个
}

// 实战:判断字符是不是数字
char input = '7';
boolean isDigit = (input >= '0' && input <= '9'); // 等价于 input >= 48 && input <= 57
System.out.println("是数字吗?" + isDigit); // true

// 实战:判断字符是不是字母
char c = 'G';
boolean isLetter = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
System.out.println("是字母吗?" + isLetter); // true
}

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+0000U+10FFFF,分成 17 个平面(Plane)

平面 范围 说明
基本平面 BMP U+0000 ~ U+FFFF 日常用到的99%的字符都在这里
补充平面 U+10000 ~ U+10FFFF emoji😀、古文字、生僻字、数学符号等

Java中的Unicode

Java的 char2字节(16位),刚好能装下BMP(基本平面)的所有字符

但emoji等补充平面的字符超过了16位,一个 char 装不下,需要两个char(代理对 Surrogate Pair)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testUnicode() {
// Java 可以直接用 Unicode 转义写字符
char a = '\u0041'; // A
char zhong = '\u4E2D'; // 中
System.out.println(a); // A
System.out.println(zhong); // 中

// 查看字符的 Unicode 码点
System.out.println("'你' 的码点:U+" + Integer.toHexString('你').toUpperCase());
// U+4F60

// emoji 需要两个 char(代理对)
String emoji = "😀";
System.out.println("emoji长度:" + emoji.length()); // 2!不是1
System.out.println("emoji码点数:" + emoji.codePointCount(0, emoji.length())); // 1
System.out.println("emoji码点:U+" + Integer.toHexString(emoji.codePointAt(0)).toUpperCase());
// U+1F600

// 所以计算含emoji的字符串真实长度,不能用 .length()
String text = "你好😀世界";
System.out.println("length()=" + text.length()); // 6(😀占了2个char)
System.out.println("真实字符数=" + text.codePointCount(0, text.length())); // 5
}

重点理解

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
2
3
4
5
6
7
8
9
10
11
12
13
14
字符"A"(U+0041):
UTF-8: 41 (1字节)
UTF-16: 00 41 (2字节)
UTF-32: 00 00 00 41 (4字节)

字符"中"(U+4E2D):
UTF-8: E4 B8 AD (3字节)
UTF-16: 4E 2D (2字节)
UTF-32: 00 00 4E 2D (4字节)

emoji "😀"(U+1F600):
UTF-8: F0 9F 98 80 (4字节)
UTF-16: D8 3D DE 00 (4字节,代理对)
UTF-32: 00 01 F6 00 (4字节)

Java实操:亲眼看字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testEncoding() throws Exception {
String text = "A中😀";

// 用不同编码方案转成字节数组,看看各占几个字节
byte[] utf8 = text.getBytes("UTF-8");
byte[] utf16 = text.getBytes("UTF-16");
byte[] gbk = text.getBytes("GBK");

System.out.println("原始字符串:" + text);
System.out.println("UTF-8 字节数:" + utf8.length); // 8 (A=1 + 中=3 + 😀=4)
System.out.println("UTF-16 字节数:" + utf16.length); // 10 (BOM=2 + A=2 + 中=2 + 😀=4)
System.out.println("GBK 字节数:" + gbk.length); // 3 (A=1 + 中=2,😀编不了会出问题)

// 打印 UTF-8 的每个字节(十六进制)
System.out.print("UTF-8 字节:");
for (byte b : utf8) {
System.out.printf("%02X ", b);
}
// 输出:41 E4 B8 AD F0 9F 98 80
}

五、乱码是怎么产生的?

一句话:用编码A写,用编码B读 → 乱码

就像你用中文写了封信,收件人用日语词典查 → 看到的全是乱七八糟的字

经典乱码场景

场景 原因 解决办法
网页显示乱码 HTML没声明charset,浏览器猜错了 <meta charset="UTF-8">
读文件乱码 文件是GBK编码,代码用UTF-8读 指定正确的编码读取
数据库乱码 数据库、连接、代码三方编码不统一 全部统一成UTF-8
控制台乱码 IDE控制台编码和程序输出编码不一致 IDEA设置里改File Encoding

Java中读写文件指定编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testFileEncoding() throws Exception {
String filePath = "test.txt";

// 写文件:指定 UTF-8 编码
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(filePath), "UTF-8"))) {
writer.write("你好世界 Hello");
}

// 读文件:必须用同样的编码,否则乱码!
// ✅ 正确:用 UTF-8 读 UTF-8 文件
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
System.out.println("UTF-8读:" + reader.readLine()); // 你好世界 Hello
}

// ❌ 错误:用 GBK 读 UTF-8 文件 → 乱码
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(filePath), "GBK"))) {
System.out.println("GBK读:" + reader.readLine()); // 浣犲ソ涓栫晫 Hello(乱码)
}
}

防乱码黄金法则:全链路统一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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──────────────────────────────────────────────────────┐
│ 字符编码全景图 │
├──────────────────────────────────────────────────────┤
│ │
│ ASCII(128个字符,1字节) │
│ │ │
│ │ 扩展 │
│ ├──→ GBK / GB2312(中文编码,1-2字节) │
│ │ └ 历史产物,老项目会遇到 │
│ │ │
│ │ 统一 │
│ └──→ Unicode(给全世界每个字符一个编号) │
│ │ │
│ │ 存储/传输方案 │
│ ├──→ UTF-8 (变长1-4字节,互联网标准)⭐ │
│ ├──→ UTF-16 (变长2-4字节,Java内部用) │
│ └──→ UTF-32 (固定4字节,几乎没人用) │
│ │
└──────────────────────────────────────────────────────┘

记住这几句话就够了

ASCII 是老祖宗,只管英文,128个字符

Unicode 是全球统一的字符编号表,兼容ASCII

UTF-8 是 Unicode 的存储方案,变长省空间,互联网标准,无脑选它

GBK 是中国的老编码,遇到老系统才需要处理

Java的char 用的是 UTF-16,所以是2字节,emoji需要两个char

乱码 = 编码和解码用了不同的方案,统一UTF-8就能根治


上一章 目录 下一章
数据类型的转换 java基础 运算符