Java 8 都发布快十年了,技术可我在上个月的每日项目里,居然还看到有人用 SimpleDateFormat 处理多线程环境下的分享日期。结果自然是技术数据错乱,线上告警响成一片。每日这让我想起一个老生常谈却又总被忽视的分享事实:Java 的很多“坑”,其实不是技术语言的问题,是每日开发者还停留在旧时代的习惯里。
就拿日期处理来说,分享SimpleDateFormat 不是技术不能用,但它不是每日线程安全的。你把它定义成 static 字段,分享多个线程共享同一个实例,技术解析和格式化的每日时候内部状态就乱了。轻则报错,分享重则静默返回错误结果——那种 bug 最难查。换成 DateTimeFormatter,人家从设计上就是 immutable 和 thread-safe 的,一行代码的事,何必赌运气。
再聊聊 Optional。我见过不少同事把它当“万能防 NPE 神器”,结果代码里全是 Optional.ofNullable(x).orElseThrow(...) 的连环套。说实话,这比原来的 if null 检查还难看。Optional 的本意是让你在 API 设计时明确表达“可能没有值”,而不是让你在业务逻辑里到处做防御。真正该用的时候,是返回值类型上,比如仓库层查不到记录,返回 Optional<User>,调用方一看就知道要处理空情况。但你要是拿它包装一个永远不会为 null 的字段,那就是给自己找麻烦。
有个细节我特别想提:Stream API 的并行流。很多人一看 parallelStream() 就觉得性能能翻倍,实际在小型数据集上,线程切换的开销比顺序执行还大。我做过一次实验,一万条数据用并行流处理,耗时反而是顺序流的 1.5 倍。核心原因在于,并行流默认使用公共 ForkJoinPool,任务太小根本不值得分派。要是真遇到大数据量的场景,你得自己评估线程池大小、任务拆分粒度,而不是无脑加个 parallel 就完事。
说个我自己的糗事。早几年写代码,习惯用 StringBuffer 拼接字符串,觉得它线程安全嘛。后来看 JVM 字节码才明白,单线程下编译器会自动把 + 转成 StringBuilder,你手动用 StringBuffer 反而多了同步锁的开销。现在写代码,除非是多线程共享同一个可变字符串(这种需求本来就少得可怜),否则一律 StringBuilder。你看,有时候“安全”的代价是性能,你得想清楚自己到底在防什么。
还有个容易忽略的点:异常处理。Java 的 checked exception 被骂了二十年,但骂归骂,你还得处理。我见过最离谱的写法是 catch 住异常后打一行日志,然后 return null。这等于把错误吞了,调用方拿到 null 还得猜是业务上没数据还是系统出故障了。要我说,要么向上抛,让上层统一处理;要么 catch 住后转成自定义的 RuntimeException,带上上下文信息。最怕的就是这种“假装处理了”的代码,它比不处理更危险——至少不处理时系统会崩溃,你能及时发现。
最后聊聊 JVM 调优。很多人一上来就调堆大小、换垃圾回收器,但大多数业务系统根本到不了那一步。我见过一个服务,频繁 Full GC,排查半天发现是代码里有个死循环在创建大对象。你说这调 JVM 参数有什么用?先写对代码,再谈优化。真要调,也得先看 GC 日志、堆转储,用数据说话,而不是凭感觉。
Java 这语言,门槛低,但水也深。你得持续更新自己的知识库,别拿十年前的写法应对今天的并发场景。工具都在那放着,就看你想不想用、会不会用了。