这是 Java 入门系列的第二天。配置好环境之后,今天进入语言的地基:计算机为什么用二进制和补码表示数据,常量与变量有什么区别,八种基本数据类型怎么选,以及类型转换中那些”丢精度”的坑。

本篇目标

  • 理解进制的本质与二进制的物理动机,熟练做进制转换
  • 讲透原码、反码、补码,明白计算机为什么统一用补码
  • 掌握常量、变量的概念与八种基本数据类型的取值范围
  • 理清自动类型提升与强制类型转换的规则和代价

进制:逢几进一的计数游戏

进制的本质是”逢 X 进一”。十进制逢十进一,源于人类有十根手指;一周七天是七进制,一年十二个月是十二进制。任何一个数都可以按”位权”展开,例如十进制的 123 其实是:

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

任意进制转十进制遵循同一个公式:系数 × 基数的权次幂之和(权从右往左、从 0 开始编号)。比如二进制 0b110:

0b110 = 1×2² + 1×2¹ + 0×2⁰ = 4 + 2 + 0 = 6

反过来,十进制转任意进制用”除基取余、倒序排列”:把 6 不断除以 2,余数依次是 0、1、1,倒过来写就是 110。

计算机为什么用二进制

不是因为二进制高级,而是它在物理上最可靠:电路只有”通电/断电”、”高电平/低电平”两种稳定状态,用 0 和 1 表示最不容易受干扰。如果要用十进制,就得让电路区分十种电压等级,稍有波动就会出错。

但二进制写起来太长,于是有了它的两种”缩写”:每 3 位二进制合成一位就是八进制(3 位最大表示 7),每 4 位合成一位就是十六进制(4 位最大表示 15,用 af 代表 1015)。规律是:进制越大,同一个数的写法越短。Java 中用前缀区分进制:

public class RadixDemo {
public static void main(String[] args) {
System.out.println(0b1100100); // 二进制,前缀 0b
System.out.println(0144); // 八进制,前缀 0
System.out.println(100); // 十进制,默认
System.out.println(0x64); // 十六进制,前缀 0x
}
}

四个输出都是 100,只是写法不同。

原码、反码、补码

计算机只存二进制,那负数怎么表示?以一个字节(8 位)表示 +7 和 -7 为例:

  • 原码:最高位当符号位(0 正 1 负),其余位表示数值。+7 是 00000111,-7 是 10000111。
  • 反码:正数与原码相同;负数符号位不变,其余位按位取反。-7 的反码是 11111000。
  • 补码:正数与原码相同;负数在反码基础上加 1。-7 的补码是 11111001。

计算机底层统一用补码存储整数,原因很深刻:补码能把减法变成加法。在 8 位的世界里,”模”是 256(就像一个 256 格的水表,转到 256 就归零),”-7” 与 “+249” 等价(因为 -7 + 256 = 249),而 249 的补码恰好和 -7 的补码是同一串比特。于是 5 - 7 可以当成 5 + (-7) 用同一套加法电路计算,CPU 不需要单独的减法器。补码还附带两个好处:0 只有一种表示(消除了 +0/-0 之争);省下来的 10000000 能多表示一个数,所以 8 位补码的范围是 -128 ~ 127,负数比正数多一个。

记住:正数的原码、反码、补码三码合一;负数的补码 = 反码 + 1。已知负数补码要求原值,逆向操作(减 1 再取反,或再求一次补码)即可。这些规则是后面看懂强制类型转换结果的前提。

常量与变量

常量是程序运行期间值不能改变的量。Java 中的字面值常量分几类:字符串常量(双引号,如 "hello")、整数常量(100)、小数常量(3.14)、字符常量(单引号且只能放一个字符,如 'A')、布尔常量(只有 true 和 false)、空常量(null,不能直接打印)。

变量是运行期间值可以改变的量,定义格式为”数据类型 变量名 = 值;”。Java 是强类型语言,每个变量必须先声明类型才能使用——类型决定了给这块内存分配多大空间、按什么规则解释里面的比特。使用变量注意三点:同一作用域内变量名不能重复;局部变量必须先赋值再使用(编译器强制检查,防止读到垃圾值);一行可以声明多个同类型变量,但可读性差,不推荐。

八种基本数据类型

类型 字节 取值范围 说明
byte 1 -128 ~ 127 最小的整数类型
short 2 -2¹⁵ ~ 2¹⁵-1 很少使用
int 4 约 ±21 亿 整数的默认选择
long 8 约 ±9.2×10¹⁸ 字面量需加后缀 L
float 4 约 ±3.4×10³⁸ 单精度,字面量需加后缀 f
double 8 约 ±1.8×10³⁰⁸ 小数的默认选择
char 2 0 ~ 65535 单个字符,Unicode 编码
boolean 未明确规定 true / false 逻辑开关

整数范围之所以是 -2³¹ ~ 2³¹-1 这种不对称的形式,正是补码的功劳。两个细节值得展开:

为什么 4 字节的 float 比 8 字节的 long 表示范围还大? 因为存储结构不同。float 按 IEEE 754 标准,32 位里 1 位是符号位、8 位是指数位、23 位是尾数位,用类似科学计数法的方式表示数值,靠指数把数量级顶上去,代价是精度损失——它能装下很大的数,但装不准。long 则是每一位都精确的整数。

char 为什么能存中文? Java 的 char 采用 Unicode 编码,占两个字节,常用汉字正好落在两字节范围内,所以 char c = '中'; 完全合法。顺带记住 ASCII 里三个常用值:'0' 是 48、'A' 是 65、'a' 是 97,char 与 int 之间可以按码值直接运算。

public class TypeDemo {
public static void main(String[] args) {
long big = 10000000000L; // 超出 int 范围的字面量必须加 L
float price = 3.14f; // 小数默认是 double,给 float 赋值要加 f
char c = '中';
System.out.println(c + 0); // 输出 20013,即"中"的 Unicode 码值
}
}

类型转换:自动与强制

自动类型提升:范围小的类型与范围大的类型运算时,先提升为大的再算,保证不丢数据。提升链是:

byte/short/char → int → long → float → double

注意第一项:byte、short、char 两两运算时都会先提升为 int。这是 JVM 的设计——字节码层面几乎没有针对 byte 的算术指令,且 byte 相加极易溢出,索性统一按 int 计算。由此引出经典面试题:

byte b1 = 3;
byte b2 = 4;
// byte b3 = b1 + b2; // 编译失败:b1+b2 已提升为 int,装不进 byte
byte b4 = 3 + 4; // 编译通过:常量优化机制

为什么变量相加不行、常量相加就行?因为编译期无法预知变量的最终值,提升后的 int 可能超出 byte 范围,编译器不敢放行;而 3 + 4 是常量,编译器在编译期直接算出 7,确认它在 byte 范围内,直接赋值——这叫常量优化机制。

强制类型转换的格式是 目标类型 变量 = (目标类型)(值);,它做的是按目标类型的位数直接截断比特位,超出范围就会得到意料之外的结果:

public class CastDemo {
public static void main(String[] args) {
byte b = (byte) 130;
System.out.println(b); // 输出 -126
}
}

130 的 int 补码是 00000000 00000000 00000000 10000010,强转 byte 只保留低 8 位 10000010,这一串作为补码恰好表示 -126——学过补码,这个结果就不神秘了。另一个高频面试题:byte b = 10; b++; 能编译,因为 ++ 运算符内部自带强制转换;而 b = b + 1; 编译失败,因为 b + 1 提升成了 int。同理,short s = 1; s += 1; 合法(扩展赋值运算符隐含强转),s = s + 1; 不合法。

字符与字符串参与运算

+ 一旦遇到字符串就变成”连接符”,且运算从左到右结合:

System.out.println('a' + 1);           // 98:char 提升为 int 做加法
System.out.println("hello" + 'a' + 1); // helloa1:先连成字符串,后面全是连接
System.out.println('a' + 1 + "hello"); // 98hello:左边先算完加法
System.out.println("5+5=" + 5 + 5); // 5+5=55:想要 5+5=10 必须加括号

小结

  • 进制是”逢 X 进一”;二进制源于电路只有两种稳定状态,八进制、十六进制是它的分组缩写。
  • 计算机用补码存整数:减法变加法、0 的表示唯一、还能多表示一个最小负数。
  • 变量必须先声明类型再使用;八种基本类型记牢字节数与范围,整数默认用 int、小数默认用 double。
  • 小类型与大类型运算自动提升,byte/short/char 先提升为 int;强转是截断比特位,可能溢出,后果自负。
  • 常量相加有编译期优化,变量相加没有;++ 和 += 隐含强转;+ 遇到字符串变连接符。