我接触过的每个 codebase 跑的是哪个 Java 版本,我都记得清清楚楚,因为大多数是我自己写的。2009 年大学期间 J2ME 用 Java 6。在 BluLogix 和 AFAQ 用 Java 7 和 8。中间某段时间用到过 Java 12。Bytro 以后一直用 Java 17。2020 年起 Kotlin 成了主力语言。

这不是语言规范的回顾。这是在每个关键节点,作为一个在职工程师,真正改变了什么——以及中间那些版本到来时,暗淡岁月是什么感觉。

Java 6:J2ME 时代,又名痛苦

我的第一个 Java 是 J2ME——Java Micro Edition——2009 年大学期间给功能机写手机 app。如果你没写过 J2ME,让我描述一下画面:你能用的子集里没有泛型,没有完整的 Collections API,屏幕抽象因设备而异,打包格式(JAR as MIDlet)让你以现代工程师从未有过的方式深刻理解字节和体积。

约束就是意义所在。超过 64 KB 的 J2ME app 会被某些运营商拒绝。我写过的布局代码,不加解释没法给任何人看。我在真机上调试,因为模拟器的错误程度足以误导你。我发布的 app 跑在堆内存 2MB 的 Nokia 手机上。

就是在这里,我学到了 JVM 的抽象不是免费的,而且只要你理解自己在付什么,就能让它们变得便宜。这个认知从那以后每天都有用。

Java 7:try-with-resources 干掉了三个完整的 bug 类别

Java 7 在 2011 年发布。等我 2013 年在 BluLogix 专业使用它时,我立刻爱上的升级是 try-with-resources

在此之前,在 Java 里关闭资源是一种特定的税:一个本身需要 null 检查的 finally 块,因为资源可能没打开成功;关闭操作可能抛异常;如果关闭时已经在处理另一个异常,原始异常就会被吞掉——你现在在调试一个幽灵。这个模式人尽皆知,也人尽皆烦:

InputStream is = null;
try {
    is = new FileInputStream(path);
    // do stuff
} catch (IOException e) {
    // handle
} finally {
    if (is != null) {
        try { is.close(); } catch (IOException ignored) {}
    }
}

Java 7 之后:

try (InputStream is = new FileInputStream(path)) {
    // do stuff
}

这不是个小便利。这是编译器在强制执行一个之前需要靠纪律和警惕才能维护的资源生命周期。我估计至少修过十几个旧 codebase 里的资源泄漏 bug,都是靠迁移到 try-with-resources 解决的。从那以后我再没写过一个这样的 bug。

Java 7 还引入了菱形运算符(<>)——你可以写 new ArrayList<>() 而不是 new ArrayList<String>()。现在看来小得可笑。当时,它消除了我每天遇到 30 次的特定摩擦。

Java 8:真正重写了我思维方式的那个

Java 8(2014 年)是我 Java 生涯中最大的转折点。不是因为 lambda 本身在抽象层面有多厉害。而是因为 lambda 在实践中开启了什么:Streams API、Optional、方法引用——以及三者组合如何改变了我推理数据转换的方式。

Java 8 之前,在 Java 里处理一个列表是 for-each 循环、本地累加器、每次访问都要 null 检查、显式构建集合。代码是正确的,但冗长到让意图难以一眼看清。

Java 8 之后:

orders.stream()
    .filter(o -> o.getStatus() == ACTIVE)
    .map(Order::getTotal)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

逻辑在 pipeline 里。你从上到下读,数据转换的形状一目了然。累积是隐式的。filter、map、reduce 都是语义清晰的具名操作。

我实际注意到的:代码 review 里的对话开始变了。不再是”这个循环看起来有问题,我们来追一下”,而是”你为什么在这里具现化,能不能保持懒加载?” 以及”这个 filter + map 可以是一个 flatMap”。这些原语改变了我们争论的对象。

Optional 是个争议点。现在还是。我喜欢把它用在返回类型上,当缺失在语义上有意义、你想迫使调用方正视这种缺失的时候。我不喜欢把 Optional 用作字段类型、方法参数或集合元素。我觉得这是正确的立场。我花了两年时间才完全到达这个立场。

Java 9 模块化时代:传奇混乱

Java 9 发布了 Java 平台模块系统(JPMS)。理论上:一种声明显式模块边界、控制你的模块暴露哪些包、构建更可维护的大型 Java 应用的方式。实践上:对任何有依赖项的人来说,一段长达六年的痛苦。

问题出在生态系统上。JPMS 要求每个库要么作为正确的具名模块发布,要么被当作”自动模块”处理——模块名是启发式推导的,可能在不同构建之间变化。Java 生态系统的大多数项目还没准备好。结果:给你的项目加上 --module-path flag,然后花一天时间解决 split-package 冲突、未命名模块,还有库依赖反射的各种 --add-opens 咒语。

我写 --add-opens java.base/java.lang=ALL-UNNAMED 的次数多到数不清。每次写,灵魂都会出走一小块。

Java 10、11、12 各自在边缘做了一些改进。Java 10 引入了局部变量类型推断(var),我立刻采用了——不是因为能省几个字符,而是因为当构造函数已经说明了类型是什么时,它阻止你把类型写两遍:

var connections = new HashMap<String, ConnectionPool>();

模块化的处境慢慢改善了。Spring、Hibernate 和其他主流框架陆续到位。等我用上 Java 17 时,JPMS 的戏剧已经基本成了背景噪音,而不是活跃事故。

Java 17:终于现代的 Java

Java 17 是一个 LTS 版本(2021 年 9 月),是第一个让我整体看 Java 时心想:这是一门现代语言的版本。

Records:

record Point(double x, double y) {}

就这样。不可变值类型,equalshashCodetoString 全部生成。不需要 Lombok。不需要 40 行的类。只需声明数据是什么。我现在到处用 records——DTO、命令对象、事件 payload、领域值对象。Java 值类型的样板代价从”烦人”变成了”零”。

密封类 + 模式匹配:

sealed interface Shape permits Circle, Rectangle {}

double area = switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.width() * r.height();
};

编译器知道 Shape 只能是 CircleRectangle。如果你新增了第三个 permits 类型却忘了在 switch 里处理它,你会得到一个编译错误。这是代数数据类型在 Java 里落地——ML 在 25 年前就有了。亡羊补牢,终为时不晚——而且 Java 的版本实用、地道,和已有类型系统集成干净。

文本块——多行字符串字面量,不用转义噩梦——Java 15 引入,我用于 SQL、测试里的 JSON 片段,以及任何超过两行的字符串。

Kotlin 的同步崛起

我想在这里说实话:Java 17 带着 records 和密封类型出现时,我在 Bytro 写 Kotlin 已经两年了,它重新校准了我的期望。数据类。类型系统内置的 null 安全。扩展函数。异步用 coroutines。Smart casts 消除了一半的 instanceof 链。

Kotlin 是 Java 的另一个自我——如果 Java 是在 2012 年而不是 1995 年从头设计的会是什么样。它跑在 JVM 上,和 Java 几乎完美互操作,修复了 Java 要到 17 才修复的大多数问题。

想写 Kotlin 时我写 Kotlin。项目是 Java 而我不想引入语言依赖时我写 Java 17。两者都是日常主力。两者底下是同一个 JVM——这意味着 GC 调优知识、堆诊断技能、thread dump 阅读、JMX 监控——全部可以迁移。JVM 是持久的投资;Java 和 Kotlin 是它上面的语言层。

15 年 JVM 实际教会你的

诚实的答案:你停止对分配感到惊讶,开始对它们保持战略性。你形成了关于垃圾回收器暂停的、可以用数字支撑的观点。你理解为什么 String interning 重要,以及它在什么时候重要、什么时候不重要。

语言改进是真实的。Java 17 比 Java 6 好写得多。但底层技能——理解成本模型、读 JVM 诊断信息、推理线程安全、为可测试性设计——这些变化的幅度远小于语法的变化。

2009 年 J2ME 教我数字节。2014 年 Streams 教我用 pipeline 思考。2021 年 Records 告诉我这门语言终于赶上了我多年来思考值类型的方式。

我还在这里。JVM 还在这里。Java 现在已经是 21 版本(LTS)了。我对虚拟线程有观点(Project Loom 是真实的,而且是好东西)。对 Java 25 会发布什么,我也会有观点。

据我所知,这就是和一个平台相处 15 年的样子。