引言
在Java的世界里,我们每天都在与整数打交道:int age = 30;
、long balance = 1000000L;
。但你是否思考过,这些数字在计算机内部的真实形态?理解原码、反码、补码
不仅是计算机科学的基础,更是深入Java底层、避免隐蔽bug的关键。本文将带你彻底掌握这些二进制表示法的奥秘及其在Java中的实际应用。
一、 基础概念:三种编码的诞生背景
计算机只能处理二进制(0和1)。如何表示有符号整数
(正数、负数)?工程师们设计了三种方案
:
1、原码
- 定义:最高位表示
符号位
(0=正数,1=负数),其余位表示数值的绝对值
- 示例 (8位byte为例):
- +5原码:
0000 0101
(符号位0,绝对值5) - -5原码:
1000 0101
(符号位1,绝对值5)
- +5原码:
- 优点:人类直观,理解容易
- 致命缺陷:
存在两种零
:+0
的原码为00000000
,-0
的原码为10000000
( 以一个字节
长表示 ),浪费表示范围,逻辑上冗余加减运算复杂
:正数和负数相加,数值部分实际上是相减,符号取决于绝对值大者的符号,硬件不能直接相加符号位和数值位
-
示例1:(+5) + (-5) = 0,程序直接运算❌
-
原码的加法
不支持符号修正
(不能自动让结果的符号与数值正确对应)java0000 0101 (+5) + 1000 0101 (-5) ----------------- 1000 1010 结果为 -10
-
示例2:(-5) + (+3) = -2,程序直接运算❌
java1000 0101 (-5) + 0000 0011 (+3) ----------------- 1000 1000 结果为 -8
-
示例3:(-5) + (-5) = -10,程序直接运算❌
-
原码的加法
不支持进位消除
(多出的进位被丢弃,不影响结果)java1000 0101 (-5) + 1000 0101 (-5) ----------------- 1 0000 1010 结果为 10(注意:最高位溢出了1位舍弃)
-
2、反码
- 定义:
- 正数:
反码 = 原码
- 负数:反码 =
负数原码符号位不变,数值位按位取反
,或者更简单点正数原码全部按位取反
- 正数:
- 示例 (8位byte为例):
- +5反码:
0000 0101
(同原码) - --5反码:
1111 1010
(-5的原码1000 0101
按位取反,符号位不变或者+5原码全部按位取反)
- +5反码:
- 遗留问题:
两种零依然存在
:+0
反码为0000 0000
,-0
反码1111 1111
(+0的原码按位取反)循环进位
:计算结果最高位有进位
时,需要把进位"回加"到最低位(称为"末位加1"或"循环进位"),硬件实现仍不理想-
示例1:(+5) + (-5) = 0,程序直接运算✅
-
反码的加法在数值上可以看作正确,但从表达角度来看,有点
不完美
java0000 0101 (+5) + 1111 1010 (-5) ----------------- 1111 1111 结果是反码,符号位不变其他按位取反,原码就是1000 0000也就是-0(负零)
-
示例2:(-5) + (+3) = -2,程序直接运行✅
java1111 1010 (-5) + 0000 0011 (+3) ----------------- 1111 1101 结果是反码,原码为1000 0010也就是-2
-
示例3:(-5) + (-5) = -10,程序直接运行❌
-
反码的加法需要
循环进位
✅java1111 1010 (-5) + 1111 1010 (-5) ----------------- 1 1111 0100 结果是反码(注意:最高位溢出了1位舍弃),最高位进位:1(需循环加回最低位) 1111 0100 + 1 ----------------- 1111 0101 这里还是反码,原码为1000 1010也就是-10
-
3、补码 🏆 - Java的选择
- 定义:
- 正数:
补码 = 原码
- 负数:
补码 = 反码 + 1
- 正数:
- 示例 (8位byte为例):
- +5补码:
0000 0101
- -5补码:+5的原码按位取反获得反码
1111 1010
,再加1获得补码1111 1011
- +5补码:
- 核心优势 (完美解决前两者问题):
- 唯一的零:
+0
补码为0000 0000
,-0
补码是+0的原码全部按位取反再加1得到还是0000 0000
,溢出一位舍去 - 减法变加法:
A - B = A + (-B)
直接成立,无需额外判断符号位
或处理循环进位
。硬件只需一套加法电路-
示例1:(+5) + (-5) = 0,程序直接运算✅
-
补码加法
支持进位消除
(多出的进位被丢弃,不影响结果)java0000 0101 (+5) + 1111 1011 (-5) ----------------- 1 0000 0001 结果是补码,先减1获得反码0000 0000,也就是0
-
示例2:(-5) + (+3) = -2,程序直接运算✅
java1111 1011 (-5) + 0000 0011 (+3) ----------------- 1111 1110 结果是补码,先减1获得反码1111 1101,符号位不变其他按位取反获取原码1000 0010,也就是-2
-
示例3:(-5) + (-5) = -10,程序直接运算✅
java1111 1011 (-5) + 1111 1011 (-5) ----------------- 1 1111 0110 结果为是补码,先减1获得反码1111 0101,符号位不变其他按位取反获得原码1000 1010,也就是-10
-
- 唯一的零:
二、 Java的坚定选择:补码一统天下
1、为什么Java整数表示使用补码?
- 补码解决了原码和反码的固有问题(
双零
、复杂运算
),简化了CPU硬件设计,提高了运算效率- 最大优势正数和负数的加法、减法可以统一处理(解决痛点:原码需要判断正负)
- 补码中只有一个 0(解决痛点:原码和反码有+0和-0,也需要单独判断)
- 补码支持进位消除(解决痛点:符号参加运行,多出的进位丢弃,不影响结果,反码需要循环进位)
- Java的所有整数类型(
byte
,short
,int
,long
)均使用补码表示!这是现代计算机体系结构的标准
2、为什么byte的范围是-128到127,而不是-127到127?
8 位二进制的表示能力
- Byte 类型占用 8 位(1 字节)存储空间,共有
2^8 = 256
种可能的二进制组合 - 在补码体系中,最高位为符号位(0 正 1 负),剩余
7
位表示数值
补码表示法的规则
- 正数和零:补码与原码相同,范围是 0000 0000(0)到 0111 1111(127),共 128 个值
- 负数:补码 = 原码取反 + 1,范围是 1000 0001(-127)到 1111 1111(-1),占 127 个值
- 关键点:
1000 0000
被定义为-128
二进制(补码) | 十进制值 |
---|---|
1000 0000 |
-128 |
1000 0001 |
-127 |
1000 0010 |
-126 |
... |
... |
1111 1110 |
-2 |
1111 1111 |
-1 |
0000 0000 |
0 |
0000 0001 |
1 |
0000 0010 |
2 |
... |
... |
0111 1110 |
126 |
0111 1111 |
127 |
为何不是 -127 到 127?
- 若范围设为 -127 到 127(含 0),仅能表示 <math xmlns="http://www.w3.org/1998/Math/MathML"> 127 + 128 = 255 127 + 128 = 255 </math>127+128=255 个值,无法覆盖全部 256 种组合
- 补码的连续性要求:将
1000 0000
分配给 -128 后:- 数值序列形成闭环:127(
0111 1111
)+1 溢出为 -128(1000 0000
),实现连续循环
- 若不这样设计,会浪费一个二进制组合(
1000 0000
),且破坏数值连续性
,-127(1000 0001
)-1正好是-128(1000 0000
)
- 数值序列形成闭环:127(
编程语言中的实际表现
- Byte.MAX_VALUE = 127,Byte.MIN_VALUE = -128
- 赋值超出范围(如 byte b = 128;)会触发编译错误,如
127 + 1 = -128
(因0111 1111 + 1 = 1000 0000
)
三、 眼见为实:Java代码验证补码
Integer.toBinaryString(int i)
方法会返回一个整数补码表示的字符串(省略前导零,负数显示完整的32位,int占4个字节
)
java
public class ComplementDemo {
public static void main(String[] args) {
int positive = 5;
int negative = -5;
// 打印正数5的二进制(补码,省略前导零)
System.out.println(Integer.toBinaryString(positive)); // 输出: 101
// 打印负数-5的二进制(32位完整补码)
System.out.println(Integer.toBinaryString(negative)); // 输出: 1111 1111 1111 1111 1111 1111 1111 1011
}
}
解读负数输出:
11111111111111111111111111111011
就是 -5 的 32 位补码- 它是由 +5 (
00000000_00000000_00000000_00000101
) 按位取反 (11111111_11111111_11111111_11111010
) - 再加 1 得到的 (
11111111_11111111_11111111_11111011
)
总结
- 原码:直观但有双零,运算复杂(历史概念)
- 反码:试图改进运算,仍有双零和循环进位问题(历史概念)
- 补码 (Java的选择):统一零表示,完美支持
A - B = A + (-B)
,硬件实现高效简单,是现代计算机整数表示的标准 - Java实践:
byte
,short
,int
,long
均用补码。Integer.toBinaryString()
可查看补码形式