恋上蓝花楹

那些年,Java 坑过我的那些事

今天是愚人节,程序员们最怕的不是同事的玩笑,而是那些藏在代码深处、等你放松警惕时突然跳出来的”坑”。作为一个在 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 的理解越来越深。

编程的乐趣,有时候就藏在这些”被坑”和”填坑”的循环里。希望这篇文章能帮你少踩几个坑,把精力留给更有价值的创造。

如果你也有印象深刻的踩坑经历,欢迎分享——毕竟今天是愚人节,分享一个坑也是一种快乐。

wulilele

我是一名热爱科技与AI的软件工程师。