今天是愚人节,程序员们最怕的不是同事的玩笑,而是那些藏在代码深处、等你放松警惕时突然跳出来的”坑”。作为一个在 Java 世界摸爬滚打多年的开发者,我决定趁着这个特殊的日子,把那些曾经”愚弄”过我的经典 Bug 和踩坑经历整理出来,希望后来者少走弯路。
一、String 的”双面人”:== 与 equals
刚入行那年,我写了这样一段代码:
String a = new String("hello");
String b = new String("hello");
if (a == b) {
System.out.println("相等");
} else {
System.out.println("不相等");
}
结果输出”不相等”,我盯着屏幕看了整整十分钟,百思不得其解。后来才明白:== 比较的是对象引用(内存地址),而不是内容。字符串比较必须用 equals()。
更坑的是字符串常量池。"hello" == "hello" 返回 true,因为字面量会被缓存复用;但 new String("hello") == new String("hello") 返回 false。这个”双面人”特性,坑了无数新手。
教训: 永远用 equals() 比较字符串内容,或者用 Objects.equals() 防止空指针。
二、Integer 缓存:-128 到 127 的”魔法区间”
这个坑更隐蔽。看这段代码:
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true
Integer m = 128;
Integer n = 128;
System.out.println(m == n); // false
同样是 == 比较,127 返回 true,128 却返回 false?原来 Java 对 -128 到 127 之间的 Integer 对象做了缓存(IntegerCache),这个范围内的自动装箱会返回同一个对象。超出这个范围,每次都会 new 一个新对象。
我曾经在一个支付系统里用 == 比较金额(Integer 类型),在测试环境(小金额)一切正常,上线后(大金额)出现了诡异的逻辑错误,排查了半天才找到这个根源。
教训: 包装类型比较,统一用 equals(),不要依赖 ==。
三、ConcurrentModificationException:边走边删的代价
某天需求是从列表中删除满足条件的元素,我写了这样的代码:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if ("b".equals(s)) {
list.remove(s);
}
}
增强 for 循环底层使用迭代器,迭代过程中直接修改集合会触发 fail-fast 机制,抛出 ConcurrentModificationException。
正确的做法有三种:
- 使用
Iterator.remove()方法 - 使用
removeIf()(Java 8+) - 使用
Stream.filter()生成新列表
// 推荐写法(Java 8+)
list.removeIf(s -> "b".equals(s));
四、SimpleDateFormat 的线程安全陷阱
这是一个在高并发场景下才会暴露的经典坑。很多人习惯把 SimpleDateFormat 定义为静态变量复用:
// 危险!线程不安全
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat 内部使用了共享的 Calendar 对象,多线程并发调用 parse() 或 format() 时会产生数据竞争,导致日期解析错误甚至抛出异常。
这个 Bug 在单线程测试时完全正常,只有在压测或生产高并发时才会偶发,极难复现和排查。
解决方案:
- 每次使用时局部创建(性能略差)
- 使用
ThreadLocal<SimpleDateFormat> - 升级到 Java 8 的
DateTimeFormatter(线程安全,推荐)
五、NPE:空指针,永远的噩梦
NullPointerException 大概是 Java 开发者最熟悉的异常了。但有些 NPE 藏得很深:
// 看似无害的拆箱操作
Map<String, Integer> map = new HashMap<>();
int value = map.get("key"); // NPE!map.get() 返回 null,自动拆箱时崩溃
自动拆箱时,如果包装类型为 null,会直接抛出 NPE,而且堆栈信息有时不够直观,让人摸不着头脑。
Java 8 引入的 Optional 是应对 NPE 的利器,但也不要滥用——它更适合作为方法返回值,而不是到处传递。
写在最后
愚人节快乐。这些坑,每一个都曾经”愚弄”过我,有些让我加班到深夜,有些让我在 Code Review 时无地自容。但正是这些踩坑经历,让我对 Java 的理解越来越深。
编程的乐趣,有时候就藏在这些”被坑”和”填坑”的循环里。希望这篇文章能帮你少踩几个坑,把精力留给更有价值的创造。
如果你也有印象深刻的踩坑经历,欢迎分享——毕竟今天是愚人节,分享一个坑也是一种快乐。