抛掉书本上街去 I:Java 完蛋了 没吃的赶紧戒(一)

整理自一些松散的发言。

有人说找个对象会好一点,因为一段积极向上的恋情对于改善人的精神状况有很大帮助。找个对象!——这也是很长一段时间里业务开发奉为圭臬的策略。过去的数十年内,面向对象的设计思想长期占据主导地位,其典型代表莫过于 Java 语言。

我最早学 Java 是在我十二岁前后,也就是国内互联网大厂(如阿里)用 Java 用的最多的那段时间,基本上那个时候学 Java 就是一个程序员迈向成功的终南捷径,网上也时常有一些诸如学 Java 年入百万之类的推文或者广告。但我并未觉得这个语言有多么魅力非凡。事实上和我当时学过的 C 和 VB 相比,它从一开始就显得又臭又长。

1
2
3
4
5
public class Helloworld {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}

然而后面我选择了微软创造的 Java 的等位替代也就是 C#,同样以 OOP 为核心思想。实际上我学 C# 的时候它同样是又臭又长,即使后面微软允许了顶层语句之类花里胡哨的特性还是如此。从我 14 岁开始,我便一直使用 C# 和 Python 作为我的首选开发语言,直到 17 岁前后学习 Rust 和 Typescript 从此在语言审美 femboy 化的路上一路狂奔为止。

我承认我确实在技术审美上不具有某些人那样极高的敏感性,这种一般的敏感性导致我居然坚持使用了 C# 整整 4 年而几乎没有发现任何猫腻,直到接触设计更现代的语言。当一个人的认知水平有限的时候,他确实难以想象到他认知以外的什么可能性。在我最早学习 C# OOP 的时候,光是理解这套设计范式就花费了我极大的精力,因为在那之前我从未接触过任何设计范式,鉴定为写 C 写的。在那个时候,我以为 OOP 就是世界的全部了。因为 OOP 确实足够建模大部分人生活中遇得到的问题,什么阿猫阿狗什么各种汽车之类的,再加上各种设计模式,就差不多了。

但是早在 21 世纪的第二个十年刚刚开始的时候,也就是我接触编程之前很久,就已经有人意识到了 OOP 作为一个范式极其严重的局限性。不少人意识到 OOP 并非 silver bullet,它在解决越来越复杂的问题上显得力不从心,过分啰嗦了。此外,OOP 并非一个性能友好的设计范式,尽管相当一部分情况下大部分人并不在乎这个问题。他们只需要套模板解决问题就行了。

2014 年一次 cppcon,某个穿着红色夏威夷度假衬衫的胖子 Mike Acton 走到台上,发表了著名的“炮打 OOP”的演讲即 Data Oriented Design and C++。他给出了一种更加先进的策略,也就是所谓面向数据设计(DOD)。当然他的这次演讲也在日后反复被人提及,例如 Zig 语言的发明者 Andrew Kelley 曾经在他作一次主题相近的演讲中途,不知从何处掏出了一件一模一样的红色衬衫披在身上,会场内一度充满了快活的空气。

神秘衬衫男

我们不妨看看 DOD 的一个十分典型的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AoS - Array of Structs
struct mob_t {
struct vec2f_t pos;
int64_t hp;
// ...
};

struct mob_t mobs[MAX_MOBS];

// SoA - Struct of Arrays
struct mob_arr_t {
struct vec2f_t pos[MAX_MOBS];
int64_t hp[MAX_MOBS];
// ...
};

struct mob_arr_t mobs;

这种设计主张使用数组之结构体(SoA)替代一般情况下大部分人会写的结构体之数组(AoS)。为什么要这样写呢?考虑以下情况。当然这个怪物可能有很多属性,也就是说这个结构体极有可能相当大,我们假设有 256 bytes。如果我们要渲染这个怪物,我们需要读取其位置数据,别的数据我们假设是不需要的。对于 AoS,每渲染一个怪物,我们就要跨越一个 256 bytes 的 stride,这几乎是四倍于一个 cache line 的大小,进一步也就导致严重的 cache miss。倘若我们的怪物很少,对于性能的影响可能并不显著。但是,倘若我们有 1000 个怪物呢?10000 个呢?在这个操作中,我们对于缓存的利用效率实际上是十分低下的,而这中间 prefetch 到 cache 中的数据甚至不是我们所需要的。相比之下,对于 SoA 方案,我们只需要跨越 8 bytes 的一个 stride,而且我们可以十分确定地说我们 prefetch 的数据几乎都是我们所需要的。而且这个对于自动向量化优化也是有好处的。至于手动向量化,你 C++ std SIMD 提案直到研究 C++26 的时候才得到通过,与此同时大伙全都写 inline asm / compiler intrinsics 去了。

写 C 其实还好,但是对于 Java 这种全是对象的语言呢?在内存里乱跳对于缓存可以说不太友好了。

只需要在这个基础上更进一步,我们就可以得到目前应用于游戏开发中的 ECS 架构。ECS 架构的主张是,设计者应当使用组件的组合而非继承链来表示实体。逻辑应当是为存储于比较高效数据结构中的某一类组件而编写,同时应当是无状态的,而非对于某个具体的实体或者状态而编写,进一步实现逻辑解耦合。另外,鉴于各个组件都在内存中比较连续,不难想到它的缓存局部性是不错的。(我自己用过的 EnTT 是基于 sparse set 的,这个在它作者的博客中有介绍,这个主要是为了方便查找,但是它内部有优化来提升缓存局部性)

下面是 pseudo code。

1
2
3
4
5
6
7
8
auto reg = registry();
auto entity = create_entity();
auto transform = MobTransform::id();
reg.emplace<MobTransform>(entity, transform);

for (const auto& t : reg.view<MobTransform>()) {
Renderer::draw_mob(t.matrix());
}

Unity DOTS 和 Bevy 都属于是这个范式的应用。然而很可惜,目前使用 ECS 架构的大型游戏仅限《守望先锋》等。我个人也使用过 ECS 开发过课程作业。然而由于使用方法不当,导致博采百家之短写的人模狗样,实在是令人发笑。

约莫半年前,Casey Muratori 发表了一个激进的演讲,也就是 The Big OOPs: An Anatomy of a 35-year Mistake。这个标题起得就很激进,当然饺子醋是什么我们也不难猜到。

Oh my gah!

一棒子把 OOP 打死显然是不对的。但是很可惜,现实正在逐渐验证 Muratori 这个如此激进的标题。我始终认为从来就不存在一个完美的范式,或许别的方案同样不是 silver bullet,但是我们目前看到的现实就是,OOP 真的愈发无法满足我们日趋复杂的软件架构需求了。同时,我也不禁怀疑,我们是否把一个自以为是的、狂妄的、自称“普世”的范式,抬到了一个远远不属于它的高度,并且不断地为这套范式打补丁,以此来强行维护它的“普世性”,强行为它挽尊呢?当然了,我们也可以继续滑坡谬误下去:Java 真的有那么好吗?

那么,Java 到底烂在哪?烂的到底是 Java 还是不负责任的程序员?

To be continued.


抛掉书本上街去 I:Java 完蛋了 没吃的赶紧戒(一)
https://lizi.moe/2026/04/11/抛掉书本上街去-I:Java-完蛋了-没吃的赶紧戒(一)/
作者
李萌
发布于
2026年4月11日
许可协议