变量与数据类型
Java 初级 · 基础语法
Java 是强类型语言:每个变量在使用前必须声明类型,且编译器会严格检查类型是否匹配。很多「诡异 bug」其实都源于对「变量在内存里到底存了什么」理解不清——比如 0.1 + 0.2 != 0.3、Integer 大数 == 比较为 false。本节不仅列结论,更从 JVM 表示和二进制根因讲透。
一、两类数据类型:基本类型 vs 引用类型
Java 类型分两大类,它们的存储方式截然不同:
关键区别一句话:基本类型的变量里直接装着「值」;引用类型的变量里装的是「对象在堆里的地址」(你可以理解为指针,但 Java 不允许你直接操作这个地址)。
二、基本类型在 JVM 里如何表示
JVM 规范规定,基本类型在局部变量表和操作数栈里以原始二进制值存放,不包装成对象。long 和 double 因为在 32 位操作数栈上占 64 位,会占用两个连续的变量槽(slot),其余类型各占一个槽。
| 类型 | 位数 | 取值范围 | 默认值 | 在栈里的表示 |
|---|---|---|---|---|
byte | 8 | -128 ~ 127 | 0 | 补码整数 |
short | 16 | -32768 ~ 32767 | 0 | 补码整数 |
int | 32 | -2³¹ ~ 2³¹-1 | 0 | 补码整数 |
long | 64 | -2⁶³ ~ 2⁶³-1 | 0L | 补码整数(占 2 槽) |
float | 32 | 约 ±3.4e38 | 0.0f | IEEE 754 单精度 |
double | 64 | 约 ±1.8e308 | 0.0d | IEEE 754 双精度 |
char | 16 | '\u0000' ~ '\uffff' | '\u0000' | UTF-16 码元 |
boolean | — | true / false | false | 编译为 int(0/1) |
关于 boolean 的一个细节
JVM 没有专门的 boolean 字节码指令。规范规定:单个 boolean 字段/变量在编译后通常当作 int 处理(0 表示 false,1 表示 true);而 boolean[] 则被实现为字节数组(每个元素 1 字节)。这就是为什么 boolean 不能做位运算——它本质是「逻辑真值」,不是数值。
整数如何表示:补码(two's complement)
Java 整数用补码存储,这解释了「为什么溢出会回绕」和「为什么 byte 能表示负数」。
以 byte(8 位)为例,200 超出范围会发生窄化:
200的二进制是1100 1000(视作无符号是 200)。- 但
byte是有符号补码,1100 1000的最高位 1 表示负数,其值为-(256 - 200) = -56。
int v = 200;
byte b = (byte) v; // 强制窄化,截断高 24 位
System.out.println(b); // 输出 -56,不是 200溢出是「静默回绕」不是「报错」
Java 的整数运算不会因溢出抛异常,而是按补码规则回绕。写累加、乘法时若数据可能超过 int 范围,请提前用 long 或在运算前强转,否则会出现「明明是正数却算出负数」的诡异结果。
三、浮点精度问题的二进制根因(重点)
这是本节最值得讲透的点。0.1 + 0.2 == 0.3 在 Java 里是 false,根因不在 Java,而在 IEEE 754 浮点数标准本身。
IEEE 754 的存储结构
double(64 位)分成三段:1 位符号 + 11 位指数 + 52 位尾数(小数有效数字)。
double 示例: 0.1
┌─┬──────────┬──────────────────────────────────────────────┐
│S│ 指数 11位 │ 尾数 52位(隐含前导 1.) │
└─┴──────────┴──────────────────────────────────────────────┘为什么 0.1 存不准
十进制 0.1 想写成「1/2 + 1/4 + 1/8 + ...」的二进制小数时,是一个无限循环小数: 0.1 = 0.0001100110011001100110011...(0011 无限循环)
但 52 位尾数只能存有限位,于是被舍入成一个极其接近但不等于 0.1 的二进制数。同理 0.2 也被舍入。两个「近似 0.1」「近似 0.2」相加,结果自然不是精确的 0.3:
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false解决方案:用 BigDecimal 表示精确小数
BigDecimal 内部用十进制整数 + 标度表示,能精确表达十进制小数,适合金额等场景。务必用 String 构造器,否则 new BigDecimal(0.1) 仍会从那个不精确的 double 拷贝过来:
import java.math.BigDecimal;
BigDecimal a = new BigDecimal("0.1"); // ✅ 用字符串,精确
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3,精确
// ❌ 错误示范:传入 double,仍携带二进制误差
BigDecimal wrong = new BigDecimal(0.1);
System.out.println(wrong); // 0.1000000000000000055511151231257827021181583404541015625金额绝对不要用 float / double 直接算
交易、计费、统计金额时,浮点误差会在累加后放大,造成「对账差一分钱」这类极难查的问题。统一用 BigDecimal(字符串构造)或数据库 DECIMAL。比较时也用 BigDecimal 的 compareTo,别用 equals(后者还比较标度)。
四、自动装箱与拆箱(字节码层面)
基本类型和对应的包装类(int↔Integer、double↔Double 等)之间可以「自动转换」,这叫自动装箱(autoboxing)/自动拆箱(unboxing)。它发生在编译期——编译器悄悄帮你插入了转换方法调用。
Integer a = 10; // 编译后: Integer a = Integer.valueOf(10); // 装箱
int b = a; // 编译后: int b = a.intValue(); // 拆箱用 javap -c 能看到:
0: bipush 10
2: invokestatic #2 // Integer.valueOf(int) ← 装箱
5: astore_1
6: aload_1
7: invokevirtual #3 // Integer.intValue() ← 拆箱Integer 缓存:为什么 127 == 127 但 200 != 200
Integer.valueOf 为了性能,对 -128 ~ 127 区间的对象做了缓存池(IntegerCache),复用同一批对象:
Integer x = 127, y = 127;
System.out.println(x == y); // true,因为指向缓存里同一个对象
Integer m = 200, n = 200;
System.out.println(m == n); // false!超出缓存,new 了两个不同对象
System.out.println(m.equals(n)); // true,比较的是内容值拆箱潜在的 NPE
自动拆箱本质调用 xxxValue(),如果包装类是 null,就会抛 NullPointerException:
Integer v = null;
int i = v; // NullPointerException!拆箱时 v.intValue() 在 null 上调用所以集合里取出的包装类(如 map.get(key) 可能为 null)赋给基本类型前要判空。
五、引用类型与内存:栈上的「地址」
引用类型的变量本身在栈(或对象字段里),它指向堆中的真实对象。理解这张图,对后面学集合、面向对象至关重要:
赋值 String a = b; 只是复制了地址,两个变量指向同一个对象——这正是「改一个引用不影响另一个变量,但改对象内容会互相影响」的原因。
六、类型转换:拓宽与窄化
- 拓宽(widening):小范围 → 大范围(如
int→long),自动完成,不会丢信息。 - 窄化(narrowing):大范围 → 小范围(如
long→int),需强制转换,可能截断或溢出。
int i = 100;
long l = i; // 拓宽,自动
int x = (int) l; // 窄化,必须显式强转一个隐藏规则:复合赋值会自动强转。x += 1 等价于 x = (typeOf x)(x + 1),所以即使 byte x = 1; x += 1; 也不会报错(但若溢出仍会回绕)。
关于 char
char 是 16 位无符号 UTF-16 码元,能直接存中文:char c = '中';。它本质是非负整数(0~65535),可以参与算术('b' - 'a' == 1),但这是码点运算,处理 emoji 等超出 BMP 的字符时要注意 char 装不下一个完整码点(需 int 码点 + codePointAt)。
七、常见误区与注意点
WARNING
- 浮点直接比较相等:
0.1 + 0.2 == 0.3永远false,用Math.abs(a - b) < 1e-9判近似相等,或金额用BigDecimal。 long漏写L:long x = 10000000000;整数字面量默认按int解析,超int范围直接编译报错,要写10000000000L。boolean与整数不互通:Java 不允许if(1)这种写法,boolean不能转成数字,也不能参与位运算。- 局部变量未初始化就用:局部变量没有默认值(字段才有),编译器强制你赋值后才能读取,否则报错。
Integer大数用==:超出 127 缓存失效,==比的是地址不是值,一律用equals。
八、最佳实践
- 默认用
int和double;超大整数用long;金额和精确小数用BigDecimal(字符串构造)。 - 强转前确认范围,警惕补码回绕;大数相乘先转
long再算。 - 比较浮点用阈值;比较包装类内容用
equals;拆箱前判null防 NPE。 - 给变量起有意义的名字,少用
a、b、tmp,提升可读性也减少误用。
📚 官方文档与参考资料
- Java SE 21 文档:https://docs.oracle.com/en/java/javase/21/
- Java 语言规范 · 基本类型:https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html
- IEEE 754 浮点标准(Wikipedia):https://en.wikipedia.org/wiki/IEEE_754
- OpenJDK 项目:https://openjdk.org/
✏️ 练习与自测
- 声明
byte、short、int、long、float、double、char、boolean各一个变量并赋值,用javap -c观察boolean在字节码里被如何处理。 - 写出
int强转byte的例子:把200、128分别转成byte,预测并用补码规则验证结果(-56、-128)。 - 用
BigDecimal计算0.1 + 0.2,对比double的结果,并解释为什么new BigDecimal(0.1)仍然不精确。 - 解释:为什么
Integer a = 127, b = 127时a == b为true,而把值改成200就变false?(结合IntegerCache与==的语义)
本节小结
Java 强类型:8 种基本类型在栈里直接存原始二进制值,整数用补码(溢出静默回绕),浮点用 IEEE 754(0.1 无法精确表示,故 0.1+0.2≠0.3,金额用 BigDecimal 字符串构造)。引用类型存的是堆中对象的地址。自动装箱/拆箱是编译器插入的 valueOf/intValue,Integer 对 [-128,127] 有缓存——所以大数比较必须用 equals。理解「值 vs 地址」「二进制表示」是避开数值类 bug 的根本。