十年,三千多个日夜,我在Java的代码堆里摸爬滚打。回头看看,真正让人夜不能寐的不是那些天花乱坠的高并发概念,而是一些晦暗角落里不起眼的坑。它们藏在每天都要写的语法和工具类里,等到上线后的深夜,才会露出獠牙。今天把这些坑摆到桌面上,看你能避开几个。
空指针不是bug,是设计缺陷
NPE是每个Java工程师的梦魇,但很少有人意识到,空指针归根结底是设计问题。一个接口允许返回null,一个方法参数不声明是否可空,一个实体类所有字段都能为空,这些不确定性积累起来,就是代码里的地雷阵。空指针不会因为你多写了几个if就消失,它只会换个地方等你。根治的方法不是在每行代码前判空,而是用Objects.requireNonNull、Optional和注解把“可空”这个决策显式化。十年后回头看,空指针最多的项目,往往也是最懒于做领域设计的项目。
并发编程的幻觉
很多人口中的“并发安全”,就是给方法加上synchronized。但锁定了方法,不代表锁定了数据;用了一个volatile,就以为原子性也有了保障。你以为加了锁就安全了,可锁本身就是一个共享的可变状态。锁粒度太粗,性能崩;锁粒度太细,死锁。线程池也是重灾区,newFixedThreadPool的队列在流量高峰时像黑洞一样吞噬内存。线程池不是池子,是沼泽,一不小心就淹死你的任务。要活下来,就得在代码里显式命名线程池、设定拒绝策略、为每个任务加超时和追踪。
Stream很爽,但别用它来折磨队友
Java 8的Stream让代码变得优雅,但优雅过了头就是灾难。Filter、map、collect一条长链,中间夹杂着复杂的Lambda,调试时只能一行行猜。用一行流解决一个复杂逻辑,你爽了,接手的人哭了。更别提parallelStream,默认使用公共的ForkJoinPool,一旦某个任务阻塞,整个应用都跟着遭殃。流是好工具,但它表达的应该是清晰的数据变换,而不是把整个业务塞进一个表达式里。
Optional不是用来当if的
Optional出现后,很多人仿佛找到了一根救命稻草,用它实现了各种if-else。代码变成了if (optional.isPresent()) { ... } else { ... },这比直接判空更让人崩溃。Optional是用来返回可能缺失的值,不是用来做if-else的语法糖。正确的用法是让返回值告诉你“有可能没有”,再通过orElse、orElseGet或orElseThrow做优雅的降级。把Optional当成容器到处传,只会让代码变得拖沓,空指针依然潜伏在某个get()上。
异常处理,别把自己包装成鸵鸟
我看过太多代码,catch住异常后打一行error,然后继续执行。某个下游接口挂了,系统照样往下走,等到了数据校验那一步,才发现脏数据已经入库。catch(Exception e) { } 是犯罪记录,不是代码。异常处理的本质是失败模式的归置,该重试的重试,该兜底的兜底,该抛出的抛出。还要小心把异常打进日志后吞掉堆栈,排查问题时看到的只剩一行没有上下文的message,那种无助感,比业务崩溃还难受。
日期时间API,别再抱着Date不放了
Java 8带来了全新的时间API,很多人却还活在SimpleDateFormat的阴影里。SimpleDateFormat是线程不安全的,共享实例时,解析结果可能错乱。用SimpleDateFormat的那个时代,连时间都不曾善待过你。新代码请无条件使用LocalDate、LocalDateTime、Instant,配合DateTimeFormatter。它们不可变、线程安全、语义清晰。历史遗留代码不能一天改完,但至少新写的每一行,都要走到光里。
框架的甜蜜陷阱
Spring Boot把复杂的配置变成了“约定优于配置”,很多工程师因此忘了框架背后的机理。他们能熟练使用@Transactional,却不知道它默认只回滚RuntimeException;他们能大谈AOP,却不知道代理对象内部自调用会失效。框架让你起飞,但也会让你飞得越高,摔得越惨。依赖注入方便了开发,也让系统里的Bean多到没人认识。遇到启动慢、循环依赖、事务失效时,先往框架的原理层钻,而不是上论坛找一个又一个注解补丁。
性能优化,别急着炫技
很多程序员对性能优化的理解还停留在“把StringBuffer换成StringBuilder”。可现实里,性能瓶颈九成以上在IO、数据库和锁竞争。改改循环里的方法调用,省下的微秒连一次网络抖动都扛不住。没有测量的优化都是耍流氓。想调优,先上profiler看CPU和内存热点,用JMH做微基准,再用压测验证假设。更需要注意的是,过早优化会把代码变成复杂度怪物,让本来清晰的逻辑变得不可维护。
代码重构,不要有偶像包袱
老项目都像一座积木塔,谁也不敢碰,生怕一抽就倒。可越害怕重构,技术债的利息就越高。很多地方不是不能改,而是没人愿意为改动的风险负责。代码写出来是给人看的,顺便给机器执行。提高可读性、消除重复逻辑、拆分长方法,这些小的重构日常化,比憋大招式的重写安全得多。十年经验告诉我,敢于重构的团队,代码质量通常也不会太差。
Java的进化,别停在Java 8
有些公司,Java版本还停留在8,连11都不敢升,更别提17和21。Java 8确实经典,但新版本带来的改进是实打实的。record消除了样板代码,switch表达式让分支更精炼,虚拟线程让高并发不再依赖堆线程池。Java 8不是终点,是起点。你以为的稳定,其实是固步自封。升级有兼容性风险,但完全不加评估地拒绝升级,等于把技术债留给下一代。
依赖管理,是一场无休止的战争
Maven和Gradle给了你依赖解析的能力,却带不走版本冲突的痛。一个传递依赖能把classpath搅得天翻地覆,两个jar包里的同名类让JVM随机加载。依赖地狱不是传说,是你pom.xml里越堆越高的山。建议是:明确管理核心依赖版本,用dependencyManagement锁定传递依赖,多测多试。遇到NoSuchMethodError或ClassNotFoundException,先查依赖树,而不是怀疑JDK。
日志,是系统的眼睛,也是系统的坟墓
日志打得好,排查问题效率飞升;日志打得烂,机器先被磁盘写满。有人在高频路径上打debug日志,有人在for循环里拼字符串,结果系统还没挂,日志先爆炸。日志不是越多越好,而是越有信息量越好。合理的日志应该包含上下文、请求ID和时间戳,格式统一,级别分明。该用debug的地方别用info,该用warn的地方别用error。为了排查问题而输出海量日志,其实是在制造新的问题。
十年Java路,坑多得数不完。每个坑都在提醒我们:写代码是在与未来的自己对话。技术选型不追新也不守旧,重要的是让代码可读、可测、可演进。避开这些坑靠的不是运气,而是对代码的敬畏。愿你踩过的坑成为你的甲胄,而不是墓碑。