拒绝生产事故!15年架构师总结的 Java “避坑指南” (万字长文建议收藏)

45 阅读5分钟

前言

写代码最尴尬的事情是什么?莫过于在本地开发环境(Dev)跑得欢快无比,一上生产环境(Prod)就报错崩溃。

很多时候,代码能跑通并不代表代码是健壮的。作为一名在 Java 领域摸爬滚打多年的“老兵”,我看过太多因为一行看似无害的代码导致的线上事故:空指针炸库、循环查库拖垮数据库、金额计算精度丢失导致财务对账失败……

今天,我将从 NPE 防御、集合操作、并发安全、数据库性能、资源管理 五个维度,总结了 17 个最容易踩的 Java 代码坑。希望能帮助大家建立“防御性编程”的思维,写出经得起生产环境考验的代码。


一、 NPE:Java 程序员的一生之敌

空指针异常(NullPointerException)是 Java 中最常见的异常。永远不要信任上游传来的数据,也不要信任数据库里取出的数据。

1. 级联调用的 NPE

❌ 错误写法

// 假设 user 或 address 或 city 任意一个为 null,直接空指针报错
String city = user.getAddress().getCity().getName();

✅ 正确写法
使用 Java 8 的 Optional 进行优雅判空。

String city = Optional.ofNullable(user)
    .map(User::getAddress)
    .map(Address::getCity)
    .map(City::getName)
    .orElse("Unknown");

2. Map 取值直接转 String

❌ 错误写法

codeJava

// 如果 map 中没有 "key"get 返回 null,调用 toString() 立即崩溃
if (map.get("key").toString().equals("1")) { ... }

✅ 正确写法

// 方式一:使用 String.valueOf (返回 "null" 字符串,虽不报错但需注意业务逻辑)
if ("1".equals(String.valueOf(map.get("key")))) { ... }

// 方式二:使用工具类 (推荐)
if ("1".equals(MapUtils.getString(map, "key"))) { ... }

3. 自动拆箱的隐形炸弹

❌ 错误写法

Integer status = null; // 数据库查出来可能是 null
// 自动拆箱:隐式调用 status.intValue() -> NPE
if (status == 1) { ... }

✅ 正确写法

// 倒过来写,或者先判空
if (Integer.valueOf(1).equals(status)) { ... }

4. equals 的“尤达条件式”

❌ 错误写法

String type = request.getParameter("type");
if (type.equals("VIP")) { ... } // 如果 type 为 null 则报错

✅ 正确写法
常量前置,像尤达大师说话一样。

if ("VIP".equals(type)) { ... } // 安全,返回 false

二、 数值与集合:细节决定成败

5. 金额计算丢失精度

涉及钱的计算,严禁使用 double 或 float。
❌ 错误写法

double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 输出 0.30000000000000004

✅ 正确写法
使用 BigDecimal,且必须传入 String 构造。

BigDecimal a = new BigDecimal("0.1"); 
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 输出 0.3

6. Integer 的 == 陷阱

❌ 错误写法

Integer a = 200;
Integer b = 200;
// 输出 false!因为超出了 -128~127 的缓存范围
System.out.println(a == b);

✅ 正确写法
对象比较永远用 equals。

System.out.println(a.equals(b)); // true

7. foreach 循环中删除元素

❌ 错误写法

for (String s : list) {
    if ("A".equals(s)) {
        list.remove(s); // 抛出 ConcurrentModificationException
    }
}

✅ 正确写法
使用迭代器或 removeIf。

list.removeIf(s -> "A".equals(s));

8. Arrays.asList 的不可变坑

❌ 错误写法

List<String> list = Arrays.asList("A", "B");
list.add("C"); // 抛出 UnsupportedOperationException

✅ 正确写法
Arrays.asList 返回的是内部类,不支持增删,需要包装一层。

List<String> list = new ArrayList<>(Arrays.asList("A", "B"));

三、 并发与多线程:隐形杀手

9. SimpleDateFormat 线程不安全

SimpleDateFormat 内部共用 Calendar 对象,多线程并发下会产生极其诡异的时间错误。
❌ 错误写法

private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

✅ 正确写法
使用 Java 8 的 DateTimeFormatter (线程安全),或者配合 ThreadLocal 使用。

10. ThreadLocal 内存泄漏

❌ 错误写法

threadLocal.set(user);
// 业务逻辑...
// 请求结束,未清理

✅ 正确写法
Tomcat 线程池是复用的,不清理会导致数据串号或内存泄漏。

try {
    threadLocal.set(user);
    // ...
} finally {
    threadLocal.remove(); // 必须清理!
}

11. 双重检查锁 (DCL) 缺失 volatile

单例模式中,如果不加 volatile,可能会因为指令重排导致其他线程获取到未初始化完全的对象。
✅ 正确写法

private static volatile Singleton instance;

四、 数据库与性能:拒绝拖垮系统

12. 循环查库 (N+1 问题)

这是新手最容易犯的性能错误。
❌ 错误写法

List<User> users = userMapper.selectAll();
for (User user : users) {
    // 循环调用数据库,如果有 1000 个用户,就查 1001 次库
    user.setOrders(orderMapper.selectByUserId(user.getId()));
}

✅ 正确写法
先批量查出所有数据,再在内存中通过 Map 匹配。

List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
List<Order> orders = orderMapper.selectByUserIds(userIds);
Map<Long, List<Order>> orderMap = orders.stream().collect(Collectors.groupingBy(Order::getUserId));
users.forEach(u -> u.setOrders(orderMap.get(u.getId())));

13. 长事务嵌套 RPC/HTTP

❌ 错误写法

@Transactional
public void buy() {
    db.decreaseStock(); // 数据库加锁
    smsService.send();  // 远程调用,耗时 2秒
    db.insertLog();     // 数据库操作
}

后果:这 2 秒内数据库连接一直被占用,高并发下连接池瞬间耗尽。
✅ 正确写法
将非 DB 操作移出事务范围。


五、 代码规范与资源管理

14. 吞掉异常

❌ 错误写法

try { ... } catch (Exception e) { e.printStackTrace(); }

✅ 正确写法
必须记录日志(Log),并根据业务决定是否抛出自定义异常,否则线上出问题根本找不到原因。

log.error("处理失败", e);

15. IO 流忘记关闭

❌ 错误写法
手动在 finally 里关闭,容易漏写或写错。
✅ 正确写法
使用 try-with-resources 自动关闭。

try (FileInputStream fis = new FileInputStream("file.txt")) {
    // ...
}

16. 循环中拼接字符串

❌ 错误写法

String str = "";
for (int i=0; i<1000; i++) { str += i; } // 产生大量垃圾对象

✅ 正确写法
使用 StringBuilder。

17. if/else 省略大括号

❌ 错误写法

if (check) return;
    doSomething(); // 缩进陷阱:这行代码永远会执行

✅ 正确写法
无论一行还是多行,必须加 {},这是对团队负责,也是对未来的自己负责。


结语

写代码不仅是和机器交流,更是和队友交流。

优秀的程序员,不是写出多么复杂的算法,而是写出健壮、清晰、可维护的代码。希望这份避坑指南能帮助大家在开发路上少走弯路,早点下班!

如果你觉得有用,欢迎点赞、收藏、转发!