代码语言

知识点思维导图

25 个知识节点

Java(33) - A1 - 附录-Java陷阱对照

你在 JS 里被 var 提升、== 隐式转换、对象引用复制坑过——Java 也有一组"长得人畜无害、踩下去很疼"的坑。本篇专治前端转 Java 最高频的四类翻车:== 比引用(String 比较)、包装类型的自动拆箱 NPE、可变对象引用共享、以及基本类型的整数溢出与除法。每个坑都给出 JS 对照,告诉你哪里"和 JS 一样别慌"、哪里"和 JS 不一样要改心智"。

一、切入点:你早就被语言坑过

JS 有它的"经典面试题坑":for (var i...) 循环里 setTimeout 全打印同一个值、[] == ![]trueconst obj2 = obj1 改一个动另一个、0.1 + 0.2 !== 0.3。Java 的坑结构上有对应,也有 JS 完全没有的新坑。把这篇当成"Java 版的怪异题集"来读最省力。

JS 的经典坑 Java 的对应坑 本篇章节
== 隐式类型转换乱套 == 比对象引用,比内容要用 .equals()
(JS 无此坑) 包装类型 Integer 自动拆箱 NPE / 缓存陷阱
obj2 = obj1 引用复制改一动二 对象/集合赋值是引用,改一动二
0.1 + 0.2 !== 0.3 浮点误差 浮点误差 + 整数溢出 + 整数除法截断

二、== 比引用:字符串/对象比较必须用 .equals()

这是前端转 Java 头号必踩坑,几乎人人栽一次。

// 场景:判断两个字符串内容是否相同
String a = new String("hello");   // a:new 出来的字符串对象
String b = new String("hello");   // b:内容相同,但是另一个独立对象

System.out.println(a == b);        // false —— ?!内容明明一样
System.out.println(a.equals(b));   // true  —— 用 equals 才对

为什么会这样(WHY):Java 的 ==对象比的是「引用」——也就是「是不是内存里同一个对象」,不是内容。ab 是两个不同的对象,地址不同,所以 == 为 false。.equals() 才比内容。

这个坑 JS 程序员其实有共鸣——JS 里 {a:1} === {a:1} 也是 false(对象比引用)。但 Java 的坑在于字符串:JS 的 "hello" === "hello" 是 true(字符串是原始类型比值),你被 JS 养成了「字符串能用 === 比」的肌肉记忆,搬到 Java 就翻车。

// JS:字符串是原始类型,=== 比值,所以能用(Java 不能照搬!)
const a = "hello";
const b = "hello";
console.log(a === b);   // true —— JS 字符串比值,惯出了错误直觉

更阴险的是「字符串常量池」会时灵时不灵

String a = "hello";        // a:字面量,进入「字符串常量池」
String b = "hello";        // b:同一个字面量,复用池里同一个对象
System.out.println(a == b);        // true —— ⚠️ 碰巧成立!因为复用了常量池同一对象

String c = new String("hello");   // c:new 强制创建新对象,不用池
System.out.println(a == c);        // false —— 换成 new 就露馅了

⚠️ 这就是坑的可怕之处:字面量字符串用 == 有时是 true(常量池复用),让你误以为 == 能用;一旦碰到 new String()、或从接口/数据库拿来的字符串,== 立刻变 false,线上 bug 就来了。别依赖常量池的巧合。

正确写法:比内容永远用 .equals()

// ✅ 正确:字符串/对象比内容,一律用 equals
if (a.equals(b)) { ... }

// ✅ 更稳:把「一定不为 null 的常量」放在前面,避免 a 为 null 时 NPE
if ("hello".equals(userInput)) { ... }   // userInput 是 null 也不会报错

// ✅ 或用 Objects.equals(两边都可能为 null 时最安全)
if (java.util.Objects.equals(a, b)) { ... }

记忆口诀:基本类型(int/double/boolean/char)用 == 比值;String 和一切对象比内容用 .equals() 这条没有例外,背下来。

边界提醒:自定义类如果没重写 equals(),默认还是比引用。项目里的实体类通常用 Lombok @Data 或 IDE 自动生成 equals(),才能正确比内容。详见第 05 篇。


三、包装类型 Integer:自动拆箱 NPE 与缓存陷阱

这是 Java 独有、JS 完全没有对应物的坑,因为 JS 的 number 只有一种。Java 的 int(基本类型)和 Integer(包装对象)混用时会咬人。

3.1 拆箱一个 null → NPE

// 场景:从 Map 里取值再做运算
Map<String, Integer> scores = new HashMap<>();   // scores:存各科分数
Integer math = scores.get("math");               // ⚠️ key 不存在,get 返回 null
int total = math + 10;                            // 💥 NullPointerException!

为什么(WHY)mathInteger(对象),值为 null。math + 10 需要把 math 拆箱成基本类型 int 才能做加法,而拆箱一个 null 就是 NPE。JS 里 undefined + 10 只会得到 NaN(不报错),Java 直接崩。

// JS:宽容,null/undefined 参与运算不崩(只是结果怪)
const math = undefined;
console.log(math + 10);   // NaN —— 不报错

正确写法:拆箱前判 null,或用 getOrDefault

// ✅ 用 getOrDefault 给默认值,避免 null
int math = scores.getOrDefault("math", 0);   // 没这个 key 就用 0
int total = math + 10;                        // 安全

3.2 用 == 比 Integer:又是引用坑 + 缓存

Integer 是对象,所以 == 比它也是比引用,而且有个「-128~127 缓存」把你骗到:

Integer a = 100;       // a:值 100,落在 -128~127 缓存区间
Integer b = 100;       // b:同样 100,复用缓存里同一个对象
System.out.println(a == b);       // true —— ⚠️ 缓存复用,碰巧成立

Integer c = 200;       // c:值 200,超出缓存区间
Integer d = 200;       // d:同样 200,各自 new 新对象
System.out.println(c == d);       // false —— ⚠️ 超出缓存,是两个对象!
System.out.println(c.equals(d));  // true  —— 用 equals 才对

为什么(WHY):Java 把 -128~127 的 Integer 预先缓存复用(和 Python 小整数缓存如出一辙)。a == b 因复用同一对象为 true,c == d 超出缓存范围是两个对象为 false。这和第一节 String 的坑同源——对象比 == 就是比引用

记忆口诀:Integer/Long 等包装类型比值也用 .equals(),别用 ==;能用基本类型 int 就用 int


四、对象/集合赋值是引用:b = a 不复制内容

这个坑 JS 程序员完全有共鸣——const b = a 对对象/数组只是复制引用,改一个动另一个。Java 一模一样。

List<Integer> a = new ArrayList<>(List.of(1, 2, 3));   // a:原列表
List<Integer> b = a;                                    // b:不是拷贝!只是同一个列表的第二个名字
b.add(4);
System.out.println(a);   // [1, 2, 3, 4] —— 动 b 也动了 a
// JS 同款行为
const a = [1, 2, 3];
const b = a;              // 只复制引用
b.push(4);
console.log(a);          // [1, 2, 3, 4]

那怎么真正复制?Java 和 JS 一样要区分浅拷贝 vs 深拷贝

List<int[]> nested = new ArrayList<>();   // nested:列表里装的是数组(引用类型)
nested.add(new int[]{1, 2});

// 浅拷贝:新建外层 List,但里面的数组还是同一批引用
List<int[]> shallow = new ArrayList<>(nested);

nested.get(0)[0] = 999;                   // 改原始数据里第 0 个数组的元素

System.out.println(shallow.get(0)[0]);    // 999 —— ⚠️ 浅拷贝跟着变了(内层数组共享)

浅拷贝(shallow):只复制最外层容器,里面的子对象还是共享引用。常见写法:new ArrayList<>(list)list.clone()new HashMap<>(map)

深拷贝(deep):递归复制所有层级。Java 没有内置的通用深拷贝(不像 JS 有 structuredClone),常见做法是逐层手动 new、实现 Cloneable、或借助序列化(如 JSON 序列化再反序列化)。

对照 JS 你就秒懂——这正是 JS 里"浅拷贝 vs 真·深拷贝"的同一组概念:

操作 JS Java 复制深度
引用赋值(不拷贝) const b = a List<T> b = a; 0 层,同一个对象
浅拷贝 [...a] / {...a} new ArrayList<>(a) / new HashMap<>(a) 1 层
深拷贝 structuredClone(a) 无内置,手动 new / 序列化绕道 全部层级

⚠️ 传参也是传引用:Java 里把一个 List 传进方法,方法内部 list.add() 会改到外面的原 List(和 JS 一样)。想不被改就传副本进去。

边界提醒:JS 的 [...a]{...a} 是浅拷贝,嵌套对象同样共享——所以这个"浅拷贝只管一层"的坑你在 JS 早见过,规则完全迁移得过来。区别只是 Java 没有 structuredClone 这种一行深拷贝。


五、数字坑:整数溢出、整数除法截断、浮点误差

JS 只有一种 number(64 位浮点),你从没操心过「整数会溢出」。Java 的 int 是 32 位整数,有上限,还有「整数除法截断」这个 JS 完全没有的坑。

5.1 整数溢出:超过 21 亿会「转圈」变负数

int max = Integer.MAX_VALUE;    // max:int 的最大值,约 21.47 亿(2147483647)
System.out.println(max + 1);    // -2147483648 —— ⚠️ 溢出!变成最小负数

// 典型翻车:两个 int 相乘超出范围
int a = 100000;                 // a:十万
int b = 100000;                 // b:十万
int result = a * b;             // 期望 100 亿,实际 1410065408 —— ⚠️ 溢出后的错误值

为什么(WHY)int 只有 32 位,最大约 21.47 亿。超过就「溢出回绕」(补码溢出),不报错、不抛异常,静默给你一个错误结果——这是最阴险的地方。JS 的 number 能安全表示到 2^53,日常根本碰不到这个上限,所以你没这根弦。

// JS:number 范围大得多,日常不会溢出
console.log(100000 * 100000);   // 10000000000 —— 正确

正确写法:可能超 21 亿的用 long

// ✅ 用 long(64 位),或让运算在 long 下进行
long a = 100000L;               // 加 L 后缀,声明为 long
long result = a * 100000;       // 10000000000 —— 正确

// ⚠️ 陷阱:int * int 先按 int 算完(已溢出)再赋给 long,晚了!
long wrong = 100000 * 100000;   // 还是 1410065408,因为右边先按 int 算
// ✅ 至少一个操作数是 long,才会用 long 运算
long right = 100000L * 100000;  // 10000000000

5.2 整数除法:/ 会截断小数(JS 没这坑)

System.out.println(5 / 2);       // 2   —— ⚠️ 不是 2.5!两个 int 相除结果截断为 int
System.out.println(5.0 / 2);     // 2.5 —— 有一个是 double 才得小数
System.out.println((double) 5 / 2); // 2.5 —— 强转一个为 double

int total = 5, count = 2;        // total 总数,count 个数
double avg = total / count;      // ⚠️ 2.0!右边 int/int 先算成 2,再转 double
double right = (double) total / count;  // ✅ 2.5,先转 double 再除

为什么(WHY):两个 int 相除,Java 认为结果也该是 int,直接砍掉小数部分(向零截断)。JS 的 5 / 2 永远是 2.5,你没这根弦,算平均值、算比例时极易翻车。

// JS:除法永远是浮点
console.log(5 / 2);   // 2.5

5.3 浮点误差:这个 JS 有共鸣

System.out.println(0.1 + 0.2);        // 0.30000000000000004 —— 和 JS 一模一样
System.out.println(0.1 + 0.2 == 0.3); // false

这个坑 JS 里你见过(0.1 + 0.2 !== 0.3),原因相同(二进制浮点表示误差),规则完全迁移。涉及金额等精确计算,用 BigDecimal

import java.math.BigDecimal;

// ⚠️ 用 String 构造,别用 double 构造(new BigDecimal(0.1) 会带入误差)
BigDecimal a = new BigDecimal("0.1");   // a:精确的 0.1
BigDecimal b = new BigDecimal("0.2");   // b:精确的 0.2
System.out.println(a.add(b));           // 0.3 —— 精确
数字坑 JS 直觉 Java 真相
整数溢出 number 到 2^53 才不安全,日常无感 ⚠️ int 约 21 亿就溢出,静默变负数;用 long
整数除法 5/2 === 2.5 ⚠️ 5/2 == 2(截断);要小数得有 double 参与
浮点误差 0.1+0.2 !== 0.3 一致;精确计算用 BigDecimal(且用 String 构造)

六、附赠几个高频小坑(认脸即可)

这些不展开,先混个眼熟,遇到不至于懵:

// 坑 1:switch 不写 break 会「贯穿」到下一个 case(JS 同款,但你可能忘)
switch (day) {
    case 1:
        System.out.println("周一");
        // ⚠️ 忘了 break,会继续执行 case 2 的代码
    case 2:
        System.out.println("周二");
        break;
}
// Java 14+ 可用 switch 表达式(箭头语法)自动防贯穿:case 1 -> "周一";

// 坑 2:遍历集合时删元素 → ConcurrentModificationException
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer x : list) {
    if (x == 2) list.remove(x);   // 💥 抛异常!不能在增强 for 里删
}
// ✅ 用 Iterator.remove() 或 list.removeIf(x -> x == 2)

// 坑 3:char 参与运算会变成 int(ASCII 码)
char c = 'A';                     // c:字符 A
System.out.println(c + 1);        // 66 —— ⚠️ 不是 "A1" 也不是 'B',是 int 66
System.out.println((char)(c + 1));// B  —— 要强转回 char

// 坑 4:数组越界不像 JS 返回 undefined,而是抛异常
int[] arr = {1, 2, 3};
// System.out.println(arr[5]);    // 💥 ArrayIndexOutOfBoundsException(JS 是 undefined)
小坑 JS 直觉 Java 真相
switch 贯穿 忘 break 会贯穿 一致,但 Java 14+ 可用 -> 语法免 break
遍历中删元素 JS 能删(行为诡异) ⚠️ 抛 ConcurrentModificationException,用 removeIf
char 运算 无 char 类型 ⚠️ char + int = int(ASCII),要强转回 char
越界访问 arr[5] 返回 undefined ⚠️ 抛 ArrayIndexOutOfBoundsException

七、总结

  • 切入点:你早就被语言坑过:| obj2 = obj1 引用复制改一动二 | 对象/集合赋值是引用,改一动二 | 三 |
  • == 比引用:字符串/对象比较必须用 .equals():这是前端转 Java 头号必踩坑,几乎人人栽一次。
  • 包装类型 Integer:自动拆箱 NPE 与缓存陷阱:这是 Java 独有、JS 完全没有对应物的坑,因为 JS 的 number 只有一种。
  • 对象/集合赋值是引用:b = a 不复制内容:这个坑 JS 程序员完全有共鸣——const b = a 对对象/数组只是复制引用,改一个动另一个。
  • 数字坑:整数溢出、整数除法截断、浮点误差:Java 的 int 是 32 位整数,有上限,还有「整数除法截断」这个 JS 完全没有的坑。
  • 附赠几个高频小坑(认脸即可):这些不展开,先混个眼熟,遇到不至于懵: