Skip to content

变量与数据类型 ​

Java 初级 · 基础语法

Java 是强类型语言:每个变量在使用前必须声明类型,且编译器会严格检查类型是否匹配。很多「诡异 bug」其实都源于对「变量在内存里到底存了什么」理解不清——比如 0.1 + 0.2 != 0.3、Integer 大数 == 比较为 false。本节不仅列结论,更从 JVM 表示和二进制根因讲透。

一、两类数据类型:基本类型 vs 引用类型 ​

Java 类型分两大类,它们的存储方式截然不同:

关键区别一句话:基本类型的变量里直接装着「值」;引用类型的变量里装的是「对象在堆里的地址」(你可以理解为指针,但 Java 不允许你直接操作这个地址)。

二、基本类型在 JVM 里如何表示 ​

JVM 规范规定,基本类型在局部变量表和操作数栈里以原始二进制值存放,不包装成对象。long 和 double 因为在 32 位操作数栈上占 64 位,会占用两个连续的变量槽(slot),其余类型各占一个槽。

类型位数取值范围默认值在栈里的表示
byte8-128 ~ 1270补码整数
short16-32768 ~ 327670补码整数
int32-2³¹ ~ 2³¹-10补码整数
long64-2⁶³ ~ 2⁶³-10L补码整数(占 2 槽)
float32约 ±3.4e380.0fIEEE 754 单精度
double64约 ±1.8e3080.0dIEEE 754 双精度
char16'\u0000' ~ '\uffff''\u0000'UTF-16 码元
boolean—true / falsefalse编译为 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。
java
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 位尾数(小数有效数字)。

text
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:

java
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 拷贝过来:

java
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)。它发生在编译期——编译器悄悄帮你插入了转换方法调用。

java
Integer a = 10;     // 编译后: Integer a = Integer.valueOf(10);   // 装箱
int b = a;          // 编译后: int b = a.intValue();               // 拆箱

用 javap -c 能看到:

text
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),复用同一批对象:

java
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:

java
Integer v = null;
int i = v;   // NullPointerException!拆箱时 v.intValue() 在 null 上调用

所以集合里取出的包装类(如 map.get(key) 可能为 null)赋给基本类型前要判空。

五、引用类型与内存:栈上的「地址」 ​

引用类型的变量本身在栈(或对象字段里),它指向堆中的真实对象。理解这张图,对后面学集合、面向对象至关重要:

赋值 String a = b; 只是复制了地址,两个变量指向同一个对象——这正是「改一个引用不影响另一个变量,但改对象内容会互相影响」的原因。

六、类型转换:拓宽与窄化 ​

  • 拓宽(widening):小范围 → 大范围(如 int → long),自动完成,不会丢信息。
  • 窄化(narrowing):大范围 → 小范围(如 long → int),需强制转换,可能截断或溢出。
java
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。

八、最佳实践 ​

  1. 默认用 int 和 double;超大整数用 long;金额和精确小数用 BigDecimal(字符串构造)。
  2. 强转前确认范围,警惕补码回绕;大数相乘先转 long 再算。
  3. 比较浮点用阈值;比较包装类内容用 equals;拆箱前判 null 防 NPE。
  4. 给变量起有意义的名字,少用 a、b、tmp,提升可读性也减少误用。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 声明 byte、short、int、long、float、double、char、boolean 各一个变量并赋值,用 javap -c 观察 boolean 在字节码里被如何处理。
  2. 写出 int 强转 byte 的例子:把 200、128 分别转成 byte,预测并用补码规则验证结果(-56、-128)。
  3. 用 BigDecimal 计算 0.1 + 0.2,对比 double 的结果,并解释为什么 new BigDecimal(0.1) 仍然不精确。
  4. 解释:为什么 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 的根本。