进制、数据存储与字符编码基础 | JavaSE

进制、数据存储与字符编码基础

一、学习目标

完成本章后,你应该能够:

  • 能够解释计算机为什么使用二进制表示和存储数据。
  • 能够区分 bit、Byte 以及常见的数据容量单位。
  • 能够完成二进制、八进制、十进制、十六进制之间的基础转换。
  • 能够正确书写 Java 中的二进制、八进制和十六进制整数。
  • 能够解释字符为什么可以用数字表示,并理解 ASCII、Unicode、UTF-8 之间的基本关系。
  • 能够理解 Java char 与 Unicode 字符之间并不是绝对的一一对应关系。
  • 能够根据进制和字符编码相关规则分析简单代码,并定位常见错误。

二、核心知识

2.1 为什么计算机使用二进制

我们平时使用的是十进制:

0 1 2 3 4 5 6 7 8 9

而计算机底层大量使用二进制:

0
1

原因并不是计算机“更喜欢 0 和 1”,而是数字电路天然适合表示两种稳定状态。

例如:

低电平 / 高电平
关闭 / 开启
无信号 / 有信号

这些状态可以抽象成:

0 / 1

因此,无论程序中保存的是:

18
'A'
"Hello"
一张图片
一段音乐
一个 Java 对象

最终都需要转换为可以由计算机存储和处理的二进制数据。

可以建立这样一条基本认识:

现实世界的信息
      ↓
程序中的数据
      ↓
数字表示
      ↓
二进制
      ↓
硬件存储和处理

2.2 bit 与 Byte

bit

bit(Binary Digit,比特)表示一个二进制位。

一个 bit 只能表示:

0

或者:

1

因此 bit 是最基本的二进制信息单位。

Byte

Byte(字节,B)由 8 个 bit 组成:

1 Byte = 8 bit

例如:

01100001

就是一个由 8 位二进制组成的字节。

Java 中:

byte

这种基本数据类型正好使用 8 位保存数据。


2.3 常见容量单位

初学阶段经常看到:

B
KB
MB
GB
TB

计算机课程中通常按照下面的方式理解:

1 KB = 1024 B
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB

原因在于:

1024 = 2^10

所以它非常适合二进制计算体系。

严格来说,在现代标准中:

1 KiB = 1024 B
1 MiB = 1024 KiB

而 SI 单位中的:

1 kB = 1000 B

但在很多操作系统、教材和日常技术交流中,仍然经常将 KB 按照 1024 B 进行理解。

学习 JavaSE 时,首先理解二进制容量的递进关系即可。


2.4 什么是进制

进制本质上是一套表示数字的规则。

“几进制”可以理解为:

使用多少种基本数字,计数到多少时产生进位。

例如十进制:

0 1 2 3 4 5 6 7 8 9

逢十进一。

二进制:

0 1

逢二进一。

常见进制如下:

| 进制 | 基数 | 可以使用的数字 | | -------- | ---: | -------------- | | 二进制 | 2 | 0、1 | | 八进制 | 8 | 0~7 | | 十进制 | 10 | 0~9 | | 十六进制 | 16 | 0~9、A~F |

十六进制中的:

A = 10
B = 11
C = 12
D = 13
E = 14
F = 15

大小写均可表示十六进制数字。


2.5 位权:理解进制转换的核心

一个数字真正的数值,不只由“这一位是什么”决定,还由“它处在哪一位”决定。

例如十进制:

123

实际上表示:

1 × 10²
+
2 × 10¹
+
3 × 10⁰

即:

100 + 20 + 3

二进制同样如此。

例如:

1101

从右向左对应:

2⁰  2¹  2²  2³

所以:

1101₂
=
1 × 2³
+
1 × 2²
+
0 × 2¹
+
1 × 2⁰

结果:

8 + 4 + 0 + 1 = 13

因此:

1101₂ = 13₁₀

这就是按权展开


2.6 十进制转换为二进制

对于非负整数,可以使用经典的:

除二取余法。

例如将十进制 13 转换成二进制。

不断除以 2:

13 ÷ 2 = 6  余 1
 6 ÷ 2 = 3  余 0
 3 ÷ 2 = 1  余 1
 1 ÷ 2 = 0  余 1

将余数倒着排列:

1101

所以:

13₁₀ = 1101₂

同理:

6₁₀ = 110₂

2.7 二进制与八进制

因为:

8 = 2³

所以每 3 个二进制位可以对应一个八进制位。

例如:

110 101

分别计算:

110₂ = 6
101₂ = 5

因此:

110101₂ = 65₈

这也是为什么八进制特别适合用来简化二进制表示。


2.8 二进制与十六进制

因为:

16 = 2⁴

所以每 4 个二进制位可以对应一个十六进制位。

例如:

1111 1010

分别计算:

1111₂ = 15 = F
1010₂ = 10 = A

因此:

11111010₂ = FA₁₆

这也是为什么开发过程中十六进制非常常见。

一个字节有 8 位:

11111111

刚好可以表示成两个十六进制数字:

FF

因此十六进制非常适合观察二进制数据。


三、使用方法

3.1 Java 中不同进制整数的写法

Java 支持直接书写二进制、八进制、十进制和十六进制整数。

public class NumberSystemDemo {
    public static void main(String[] args) {
        int binary = 0b0110_0001;
        int octal = 0141;
        int decimal = 97;
        int hexadecimal = 0x61;

        System.out.println(binary);
        System.out.println(octal);
        System.out.println(decimal);
        System.out.println(hexadecimal);
    }
}

四个变量保存的数值实际上完全相同:

97
97
97
97

对应关系如下:

0b01100001
    ↓
  二进制

0141
 ↓
八进制

97
↓
十进制

0x61
 ↓
十六进制

Java 的整数进制前缀如下:

| 进制 | Java 写法 | | -------- | ------------ | | 二进制 | 0b0B | | 八进制 | 前导 0 | | 十进制 | 无特殊前缀 | | 十六进制 | 0x0X |


3.2 使用下划线提高数字可读性

Java 允许在数字之间使用下划线:

int binary = 0b0110_0001;

int money = 1_000_000;

long population = 8_000_000_000L;

下划线不会改变数字本身的值,只用于增强可读性。

例如:

1_000_000

和:

1000000

数值完全一样。


3.3 使用 Java API 查看不同进制

除了手算之外,Java 也提供了相关工具方法。

public class RadixApiDemo {
    public static void main(String[] args) {
        int number = 97;

        System.out.println(Integer.toBinaryString(number));
        System.out.println(Integer.toOctalString(number));
        System.out.println(Integer.toHexString(number));
    }
}

得到:

1100001
141
61

还可以将指定进制字符串解析为整数:

int binary = Integer.parseInt("1100001", 2);

int hexadecimal = Integer.parseInt("61", 16);

System.out.println(binary);
System.out.println(hexadecimal);

得到:

97
97

这里的:

2
16

表示解析时使用的 radix(基数)。


3.4 字符为什么可以参与数字运算

计算机最终只能处理数字,因此字符也必须建立:

字符 ↔ 数字

之间的映射关系。

最经典的字符编码标准之一就是 ASCII:

American Standard Code for Information Interchange

常见字符对应关系:

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

例如:

public class CharacterNumberDemo {
    public static void main(String[] args) {
        char ch = 'A';

        System.out.println(ch);
        System.out.println((int) ch);
        System.out.println(ch + 1);
    }
}

结果:

A
65
66

其中:

(int) ch

可以直接观察 char 对应的数值。

而:

ch + 1

进入数值运算后,char 会参与数值类型提升,因此结果是:

66

而不是:

B

具体的类型提升规则将在下一章继续学习。


3.5 '0'0"0" 完全不同

这是初学者非常容易混淆的问题。

char a = '0';

int b = 0;

String c = "0";

三者分别是:

'0'   → char
0     → int
"0"   → String

其中:

'0'

对应的数值是:

48

所以:

System.out.println('0' + 1);

结果不是:

1

而是:

49

四、原理与进阶

4.1 有符号整数为什么会出现负数

对于 byteshortintlong 等 Java 有符号整数,计算机采用二进制表示正数和负数。

Java 整数采用二进制补码(Two's Complement)语义。

例如 byte 一共有 8 位。

它能够表示:

-128 ~ 127

也就是:

-2⁷ ~ 2⁷ - 1

为什么不是:

0 ~ 255

因为 Java 的 byte有符号整数类型

这一机制会直接影响后面学习的:

  • 强制类型转换
  • 整数溢出
  • 位运算
  • 原码、反码、补码
  • 网络数据处理

下一章看到:

int value = 1500;
byte result = (byte) value;

结果变成负数时,真正的根源就在这里。


4.2 ASCII、Unicode 与 UTF-8 不完全是一回事

这三个概念经常被混在一起。

可以先建立最重要的层次关系。

ASCII

ASCII 主要解决早期英文字符、数字和符号的编码问题。

例如:

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

Unicode

随着计算机走向全球化,仅有 ASCII 已经不够了。

需要表示:

中文
日文
韩文
阿拉伯文
Emoji
各种数学符号
……

于是需要一个统一的字符体系。

Unicode 的核心目标可以简单理解为:

给世界上的字符分配统一的编号。

这样的编号称为:

Unicode Code Point(Unicode 码点)

例如字符可能表示为:

U+0041
U+4E2D

UTF-8

UTF-8 则解决:

Unicode 字符编号真正转换成字节之后应该如何保存。

因此从概念上可以理解为:

Unicode
负责“字符编号是什么”

UTF-8
负责“这些编号怎样编码成字节”

UTF-8 是一种变长编码:

1~4 Byte

并且与 ASCII 的基本字符保持兼容。

字符集和字符编码将在 File 与 IO 阶段继续系统学习,本章只需要建立概念框架。


4.3 Java 的 char 并不一定等于一个完整 Unicode 字符

这是一个非常重要的 Java 细节。

Java:

char

使用 16 位保存一个 UTF-16 code unit。

对于大量常用字符,例如:

A
中
国

一个 char 就足够表示。

但是 Unicode 的范围已经超过了 16 位。

某些字符,例如部分 Emoji,需要两个 char 才能表示。

例如:

public class UnicodeDemo {
    public static void main(String[] args) {
        String text = "😀";

        System.out.println(text);
        System.out.println(text.length());
    }
}

你可能直觉上认为:

😀

只有“一个字符”。

String.length() 得到的是 UTF-16 code unit 的数量,因此这里会得到:

2

所以以后处理 Unicode 文本时:

一个 char

不能简单等价于:

人眼看到的一个字符

这是 Java 字符串处理中的重要边界。


4.4 所有文件最终为什么都能保存为二进制

图片、音频、视频看起来与数字毫无关系,但磁盘最终保存的仍然是字节。

例如图片可以记录:

宽度
高度
像素
颜色
透明度
压缩信息

音频可以记录:

采样率
采样位数
声道
声音采样数据

视频又可以记录:

画面
声音
时间信息
压缩编码

这些信息最终都会按照文件格式约定转换成字节序列。

因此:

同一串 0 和 1

本身没有天然的“图片”或“音乐”含义。

真正决定含义的是:

数据格式 + 解析规则

可以把文件理解成:

按照某种协议组织起来的一串字节。


五、实践应用

进制和编码并不是只存在于考试题中。

在真实开发中,十六进制经常出现在:

颜色值
内存地址
二进制协议
文件格式
哈希值
调试信息
网络数据
权限标志

例如 Web 中常见:

#FF0000

表示红色。

其中:

FF = 255

而在调试底层数据时:

0xFF
0xCA
0xFE

往往比一长串二进制更加容易阅读。

随着后面学习:

  • 位运算
  • IO
  • 网络编程
  • JVM
  • 字符编码

你会不断重新遇到本章知识。

所以进制不是孤立知识,而是理解计算机底层数据表示的入口。


六、常见问题

6.1 为什么 010 不是十进制 10?

因为 Java 中:

int number = 010;

前导 0 表示八进制。

所以:

010₈ = 8₁₀

实际开发中如果没有明确需要,通常不建议使用八进制字面量表示普通数字,以免产生阅读歧义。


6.2 08 为什么不能作为普通整数直接写?

八进制只能出现:

0~7

所以:

int number = 08;

是不合法的整数写法。


6.3 0x10 等于多少?

十六进制:

0x10

等于:

1 × 16¹ + 0 × 16⁰

因此数值是:

16

6.4 'A' 本身保存的是 65 吗?

在数值语义上:

char ch = 'A';

ch 对应的 UTF-16 code unit 数值是:

65

因此可以转换成:

int

进行数值处理。


6.5 ASCII 和 Unicode 谁替代谁?

不能简单理解成“Unicode 把 ASCII 删除了”。

Unicode 的基本拉丁字符区与 ASCII 建立了兼容关系。

例如:

A → 65
a → 97
0 → 48

这些经典编号仍然保持一致。


6.6 Unicode 和 UTF-8 是不是同一个概念?

不是。

简单记忆:

Unicode → 字符编号体系
UTF-8   → Unicode 的一种编码方式

以后学习 IO 时会进一步研究编码和解码。


6.7 一个 char 是否永远等于一个字符?

不是。

char 是:

16 位 UTF-16 code unit

某些 Unicode 字符需要两个 char 表示。

因此不要建立:

1 char = 世界上任意一个字符

这样的绝对认识。


七、练习与验收

7.1 知识问答

  1. 计算机为什么适合使用二进制表示和存储数据?
  2. bit 与 Byte 分别是什么?二者有什么关系?
  3. 为什么二进制每 3 位可以快速转换为一个八进制位?
  4. 为什么二进制每 4 位可以快速转换为一个十六进制位?
  5. Java 中二进制、八进制和十六进制整数分别使用什么前缀?
  6. ASCII 是什么?字符 'A''a''0' 分别对应什么数值?
  7. '0'0"0" 分别是什么类型?
  8. Unicode 与 UTF-8 分别解决什么问题?
  9. 为什么图片、音频、视频最终都可以保存成二进制数据?
  10. 为什么不能简单认为一个 Java char 永远代表一个完整 Unicode 字符?
  11. 请将 0b0110000101410x61 转换成十进制,并判断三者之间有什么关系。
  12. 为什么 Java 代码中的 01010 数值不同?

7.2 代码阅读

禁止运行下面的代码,先预测结果:

public class BinaryRead01 {
    public static void main(String[] args) {
        int a = 0B01100001;
        int b = 0141;
        int c = 0X61;
        char ch = 'A';

        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(ch + 1);
    }
}

完成:

  1. 写出四行输出结果。
  2. 解释为什么三种不同进制写法可以表示相同数值。
  3. 解释为什么 ch + 1 的结果不是直接显示字符 B

继续分析:

public class RadixRead02 {
    public static void main(String[] args) {
        int a = 0b1010;
        int b = 012;
        int c = 0xA;

        System.out.println(a == b);
        System.out.println(b == c);
    }
}

要求:

  1. 不运行代码预测结果。
  2. 分别写出三个整数的十进制值。
  3. 解释变量保存的是“进制形式”还是最终数值。

7.3 手写代码

  1. 不查询资料,手算十进制 6、13、37、255 的二进制形式。
  2. 手写程序分别定义二进制、八进制、十进制、十六进制整数,使四个变量保存相同数值。
  3. 使用 Integer.toBinaryString()Integer.toOctalString()Integer.toHexString() 输出整数 255 的三种表示形式。
  4. 编写程序输出 'A''a''0' 对应的整数值。
  5. 编写程序验证一个普通中文字符和一个 Emoji 在 String.length() 上的差异。

7.4 Debug

下面代码存在问题:

public class RadixDebug01 {
    public static void main(String[] args) {
        int number = 08;

        System.out.println(number);
    }
}

要求:

  1. 判断问题发生在编译期还是运行期。
  2. 找出错误涉及哪一种进制规则。
  3. 分别修改成合法的十进制 8 和八进制 8 的表示方式。

继续分析:

public class CharacterDebug02 {
    public static void main(String[] args) {
        char value = 'AB';

        System.out.println(value);
    }
}

要求:

  1. 判断代码是否合法。
  2. 说明 char 字面量的基本限制。
  3. 如果需要表示 "AB",应该考虑使用什么类型?

7.5 综合训练

设计一个“数字表示观察器”。

暂时不使用键盘输入,固定:

int number = 97;

程序需要输出:

十进制形式
二进制形式
八进制形式
十六进制形式
如果将该数字转换成 char,对应什么字符

要求:

  1. 合理拆分代码。
  2. 输出格式清晰。
  3. 不允许直接把所有答案写死在字符串中。
  4. 完成后说明十进制 97 与字符 'a' 之间的关系。

7.6 本章验收

在不查看资料的情况下完成:

  • [ ] 能解释二进制为什么成为计算机底层数据表示基础。
  • [ ] 能说出 bit、Byte 的关系。
  • [ ] 能手算基础的二进制与十进制转换。
  • [ ] 能解释八进制与十六进制为什么方便表示二进制。
  • [ ] 能正确书写 Java 的二进制、八进制和十六进制整数。
  • [ ] 能解释 ASCII 中 'A''a''0' 的经典编号。
  • [ ] 能区分 ASCII、Unicode 和 UTF-8 的基本角色。
  • [ ] 能解释为什么 char + 1 会产生整数结果。
  • [ ] 能识别 Java 八进制前导 0 带来的常见问题。
  • [ ] 能解释为什么一个 char 不一定等价于一个完整 Unicode 字符。

如果以上内容仍有多项需要查资料才能完成,则本章暂时不应视为真正掌握。