- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
本文以开源仓库 java-design-patterns 中
bytecode模块的官方文档(阿拉伯语版与 英语版)为核心,结合模块源码与测试,系统讲解 Bytecode(字节码)模式的设计意图、指令集定义、栈式虚拟机实现、文本指令转换工具,以及完整的可运行示例与验证方式。读完本文,你将掌握如何把"行为"编码为数据、由虚拟机逐条解释执行,从而在游戏开发等场景中实现数据驱动的行为定制。
一、模式意图:把行为编码为指令
Bytecode 模式的核心意图是:允许将行为编码为虚拟机可执行的指令。在传统开发中,行为以源码形式固化在代码里,每次调整都需要重新编译;而 Bytecode 模式把行为变成"数据",由运行时解释执行,从而让行为获得数据才有的灵活性——可以存储、传输、动态组合,甚至由非程序员(如游戏策划)通过文本指令来定义。
用一句话概括(文档原话):Bytecode 模式让行为由数据驱动,而非由代码驱动(Bytecode pattern enables behavior driven by data instead of code)。
现实世界的类比
一支团队正在开发一款巫师对战的新游戏。巫师的行为需要通过反复试玩调整、迭代数百次。如果每次游戏策划想调整行为都要请程序员改代码,那显然不理想。因此,巫师的行为被实现为一台"基于数据的虚拟机"。
这正是 Bytecode 模式的典型动机:把频繁变动的游戏逻辑从编译期代码中剥离,交给运行时解释的数据指令。
Game Programming Patterns 对 Bytecode 模式的定义(文档引用)是:
指令集(instruction set)定义了可执行的低层操作。一系列指令被编码为字节序列。虚拟机一条一条地执行这些指令,并用一个栈存放中间值。通过组合指令,可以定义出复杂的高层行为。
二、仓库模块结构与运行方式
本仓库的bytecode模块是一个独立的 Maven 子项目,代码结构如下:
- bytecode/src/main/java/com/iluwatar/bytecode/:核心实现,包含
Wizard、Instruction、VirtualMachine、App四个类; - bytecode/src/main/java/com/iluwatar/bytecode/util/InstructionConverterUtil.java:把文本形式的指令字符串转换为字节码数组的转换工具;
- bytecode/src/test/java/com/iluwatar/bytecode/:JUnit 5 测试,覆盖虚拟机各指令行为与转换工具;
- bytecode/etc/:类图(
bytecode.urm.png)与序列图(bytecode-sequence-diagram.png)资源。
运行演示程序时,可直接执行App类的main方法(在模块根目录 bytecode/pom.xml 下通过 Maven 构建后运行),其输出即本文第四节展示的栈轨迹日志。
三、核心参与者:三个关键类
1. Wizard——被操作的游戏对象
Wizard.java 是游戏对象模型,持有三类可被指令修改的属性,以及两个副作用行为计数器:
@AllArgsConstructor @Setter @Getter @Slf4j public class Wizard { private int health; // 生命值 private int agility; // 敏捷 private int wisdom; // 智慧 private int numberOfPlayedSounds; // 已播放音效次数 private int numberOfSpawnedParticles; // 已生成粒子次数 public void playSound() { LOGGER.info("Playing sound"); numberOfPlayedSounds++; } public void spawnParticles() { LOGGER.info("Spawning particles"); numberOfSpawnedParticles++; } }Wizard自身没有任何业务逻辑判断,它只是一个"数据容器 + 行为原语",完全被动地接受虚拟机的指令操作。
2. Instruction——指令集枚举
Instruction.java 用 Java 枚举定义了 11 条指令,每条指令绑定一个整数操作码。每一条指令都有自己操作栈数据的语义,例如ADD弹出栈顶两个值求和后压回栈。完整的指令码表如下:
| 操作码 | 指令 | 栈语义 |
|---|---|---|
| 1 | LITERAL | 读取 bytecode 中下一个值并压入栈(如LITERAL 0压入 0) |
| 2 | SET_HEALTH | 弹出生命值数值与巫师编号,调用 set health |
| 3 | SET_WISDOM | 弹出智慧值数值与巫师编号,调用 set wisdom |
| 4 | SET_AGILITY | 弹出敏捷值数值与巫师编号,调用 set agility |
| 5 | PLAY_SOUND | 弹出值作为巫师编号,调用 play sound |
| 6 | SPAWN_PARTICLES | 弹出值作为巫师编号,调用 spawn particles |
| 7 | GET_HEALTH | 弹出值作为巫师编号,将巫师生命值压入栈 |
| 8 | GET_AGILITY | 弹出值作为巫师编号,将巫师敏捷值压入栈 |
| 9 | GET_WISDOM | 弹出值作为巫师编号,将巫师智慧值压入栈 |
| 10 | ADD | 弹出两个值,压入它们的和 |
| 11 | DIVIDE | 弹出两个值,压入它们的商 |
枚举还提供了反向查找方法getInstruction(int value):遍历所有枚举项,找到intValue匹配的指令;找不到时抛出IllegalArgumentException("Invalid instruction value")。
3. VirtualMachine——解释执行的核心
VirtualMachine.java 是本模式的心脏:它持有一个操作数栈Stack<Integer>和一个长度为 2 的巫师数组Wizard[] wizards,并提供两个构造函数:
- 无参构造:用
ThreadLocalRandom在[3, 32]区间为两名巫师随机生成 health、agility、wisdom 属性; - 有参构造:直接接收两个
Wizard实例(演示程序采用此方式,保证可复现)。
execute(int[] bytecode)方法是解释器的核心循环:逐条读取 bytecode 中的整数,通过Instruction.getInstruction解析出枚举值,再用switch分支执行对应的栈操作:
public void execute(int[] bytecode) { for (var i = 0; i < bytecode.length; i++) { Instruction instruction = Instruction.getInstruction(bytecode[i]); switch (instruction) { case LITERAL -> { // 读取下一个字节作为立即数 int value = bytecode[++i]; stack.push(value); } case SET_HEALTH -> { // 弹出数值与巫师编号,写入属性 var amount = stack.pop(); var wizard = stack.pop(); setHealth(wizard, amount); } case GET_HEALTH -> { // 弹出巫师编号,将属性压栈 var wizard = stack.pop(); stack.push(getHealth(wizard)); } case ADD -> { // 弹出两值求和压栈 var a = stack.pop(); var b = stack.pop(); stack.push(a + b); } case DIVIDE -> { // 弹出两值做除法压栈(注意 b / a) var a = stack.pop(); var b = stack.pop(); stack.push(b / a); } case PLAY_SOUND -> { // 弹出巫师编号,触发行为 var wizard = stack.pop(); getWizards()[wizard].playSound(); } // ... 其余指令分支 default -> throw new IllegalArgumentException("Invalid instruction value"); } LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack()); } }从源码可看出两点关键设计:其一,栈是后进先出(LIFO)的,SET_*类指令先弹数值再弹巫师编号,对应文本指令中的书写顺序;其二,DIVIDE的分支是stack.push(b / a),即先弹出的作为除数、后弹出的作为被除数,这也解释了演示程序里"先压入 11 再压入 2"能得到 11 ÷ 2 = 5(整数除法向下取整为 5,但示例中 18 ÷ 2 = 9)。每执行一条指令,虚拟机都会打印一次当前栈内容,方便逐条追踪。
4. InstructionConverterUtil——文本指令到字节码的桥梁
为了让"数据驱动的行为"真正可写、可读,仓库提供了一个转换工具 InstructionConverterUtil.java。convertToByteCode(String instructions)把空格分隔的文本指令串转换为int[]字节码数组:
- 输入为
null或空白串时返回空数组; - 对每个分词,先尝试按
Instruction.valueOf解析为指令名(合法则取其intValue); - 再尝试
Integer.parseInt解析为立即数(如LITERAL 0中的0); - 两者都不匹配时抛出
IllegalArgumentException("Invalid instruction or number: " + token)。
例如"LITERAL 0 GET_HEALTH"会被转换为[1, 0, 7]。该行为由 InstructionConverterUtilTest.java 验证:testInstructions断言"LITERAL 35 SET_HEALTH ..."转换出的数组长度与各位置操作码完全符合预期;testEmptyInstruction断言空串转换结果长度为 0。
四、完整可运行示例与栈轨迹解析
以下完整示例来自 App.java(文档亦收录):
public static void main(String[] args) { var vm = new VirtualMachine( new Wizard(45, 7, 11, 0, 0), // 0 号巫师:health=45, agility=7, wisdom=11 new Wizard(36, 18, 8, 0, 0)); // 1 号巫师:health=36, agility=18, wisdom=8 vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("GET_HEALTH")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("GET_AGILITY")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 0")); vm.execute(InstructionConverterUtil.convertToByteCode("GET_WISDOM")); vm.execute(InstructionConverterUtil.convertToByteCode("ADD")); vm.execute(InstructionConverterUtil.convertToByteCode("LITERAL 2")); vm.execute(InstructionConverterUtil.convertToByteCode("DIVIDE")); vm.execute(InstructionConverterUtil.convertToByteCode("ADD")); vm.execute(InstructionConverterUtil.convertToByteCode("SET_HEALTH")); }这段程序计算的是0 号巫师的综合属性均值:(health + agility + wisdom) / 2 = (45 + 7 + 11) / 2 = 31.5,由于是整数除法(栈上先压 11 后压 2,DIVIDE计算b / a),得到63 / 2 = 31,最终再加回生命值得到新值45 + 31 = 76,写入 0 号巫师的 health。控制台输出(文档原样收录)逐条展示了每一步的栈变化:
16:20:10.193 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0] 16:20:10.196 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 0] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_HEALTH, Stack contains [0, 45] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 0] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_AGILITY, Stack contains [0, 45, 7] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 7, 0] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed GET_WISDOM, Stack contains [0, 45, 7, 11] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 45, 18] 16:20:10.197 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed LITERAL, Stack contains [0, 45, 18, 2] 16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed DIVIDE, Stack contains [0, 45, 9] 16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed ADD, Stack contains [0, 54] 16:20:10.198 [main] INFO com.iluwatar.bytecode.VirtualMachine - Executed SET_HEALTH, Stack contains []逐行解读这段输出,可以帮助你理解栈式虚拟机的完整执行过程:
LITERAL 0压入巫师编号 0 → 栈[0];LITERAL 0压入第一个操作数 0(占位)→ 栈[0, 0];GET_HEALTH弹出 0 号巫师,压入其 health 45 → 栈[0, 45];LITERAL 0再次压入巫师编号 → 栈[0, 45, 0];GET_AGILITY弹出 0 号巫师,压入 agility 7 → 栈[0, 45, 7];LITERAL 0压入巫师编号 → 栈[0, 45, 7, 0];GET_WISDOM弹出 0 号巫师,压入 wisdom 11 → 栈[0, 45, 7, 11];ADD弹出 11 与 7,压入 18 → 栈[0, 45, 18];LITERAL 2压入除数 2 → 栈[0, 45, 18, 2];DIVIDE弹出 2 与 18,压入 18/2=9 → 栈[0, 45, 9];ADD弹出 9 与 45,压入 54 → 栈[0, 54];SET_HEALTH弹出 54 与 0,执行setHealth(0, 54)→ 栈清空[]。
五、类图与执行时序
模块的类图(bytecode/etc/bytecode.urm.png)展示了App、VirtualMachine、Wizard、Instruction、InstructionConverterUtil之间的协作关系:App依赖VirtualMachine与InstructionConverterUtil;VirtualMachine持有一个Stack<Integer>与Wizard[]数组,并依赖Instruction枚举。
时序图(bytecode/etc/bytecode-sequence-diagram.png)则描绘了运行时消息流:App将文本指令交给InstructionConverterUtil转换为字节码数组,VirtualMachine.execute逐条解析并操作Wizard的属性。
六、什么时候使用 Bytecode 模式
文档指出,当你需要定义大量行为,而游戏所使用的实现语言不适合直接编写这些行为时,应当考虑 Bytecode 模式,典型触发条件如下:
- 语言层级过低:直接用实现语言编写行为过于繁琐、容易出错;
- 迭代成本过高:由于编译速度慢或其他工具链问题,反复修改行为需要太长时间;
- 信任度问题:如果希望确保被定义的行为不可能破坏游戏(沙箱隔离),需要将行为代码与其余代码库隔离。
这三个条件正好对应游戏策划高频调参、需要热更新行为、且希望行为运行在受控环境中的场景。
七、已知的真实应用
文档列举了三种广为人知的 Bytecode 使用案例:
- Java 虚拟机(JVM):使用 bytecode 让 Java 程序可以在任何安装了 JVM 的设备上运行(这正是"可移植性"的教科书级例子);
- Python:将脚本编译为 bytecode,再由 Python 虚拟机解释执行;
- .NET Framework:使用名为 Microsoft Intermediate Language(MSIL)的字节码形式。
这些案例共同印证了 Bytecode 模式"一次编码、处处解释"的价值。
八、后果分析:优点与代价
优点
- 可移植性(Portability):程序可以在任何装有兼容虚拟机的平台上运行;
- 安全性(Security):虚拟机可以在解释执行时对字节码实施安全检查(沙箱化);
- 性能潜力(Performance):JIT(即时编译器)可以在运行时优化字节码,相较纯解释执行可能带来性能提升。
缺点
- 开销(Overhead):运行字节码通常比运行原生代码有更多额外开销,可能影响性能;
- 复杂度(Complexity):实现和维护一个虚拟机为系统增加了额外复杂度。
这两点与"数据驱动灵活性"构成了经典的权衡:灵活与安全来自 VM 这一层间接,而其代价正是间接层的开销与维护成本。
九、相关设计模式
文档明确指出 Bytecode 模式与以下三个模式存在紧密联系,本仓库均有对应模块:
- 解释器(Interpreter):虚拟机实现内部通常使用解释器模式来解析并执行字节码指令——本模块
VirtualMachine.execute对Instruction的switch分发就是解释执行的具体形态; - 命令(Command):每条字节码指令都可以看作一个由虚拟机执行的动作命令,指令即命令的编码形式;
- 工厂方法(Factory Method):虚拟机可能使用工厂方法来实例化字节码中定义的操作或指令。
十、测试验证:行为与转换的双重保障
模块的测试代码是理解本模式正确性的最佳佐证,位于 bytecode/src/test/java/com/iluwatar/bytecode/:
- VirtualMachineTest.java:逐条验证虚拟机行为,例如
testLiteral断言执行[LITERAL, 10]后栈中弹出 10;testSetHealth用[LITERAL, 0, LITERAL, 50, SET_HEALTH]断言 0 号巫师 health 变为 50;testPlaySound断言执行后numberOfPlayedSounds递增为 1 且栈被清空;testInvalidInstruction断言操作码 999 触发IllegalArgumentException; - InstructionConverterUtilTest.java:验证文本到字节码的转换正确性与空输入处理;
- AppTest.java:验证主程序可正常启动。
结语
Bytecode 模式的价值在于把行为从"代码"降维成"数据":指令集是语法的边界,栈式虚拟机是执行语义的载体,文本转换工具则是人与机器之间的翻译层。在 java-design-patterns 的bytecode模块中,你可以通过 Wizard.java、Instruction.java、VirtualMachine.java 三份源码完整复现这套机制,再结合测试用例理解其行为契约。当你在游戏或规则引擎项目中遇到"行为需要频繁调整、又希望隔离风险"的需求时,这套数据驱动的虚拟机思路将是一个值得借鉴的架构选择。
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
Craft Agents 许可与商标解读:Apache 2.0下你能做什么不能做什么
Craft Agents 许可与商标解读:Apache 2.0下你能做什么不能做什么 Craft Agents 是一款支持多会话、多模型、MCP 与自动化工作流
示例工程教程Serial Studio 的 Trust Contract(信任契约):用五条规则把 AI 协作者从"可审查"变成"可依赖"
Serial Studio 的 Trust Contract(信任契约):用五条规则把 AI 协作者从"可审查"变成"可依赖" 本文以 Serial Studi
桌面应用数据可视化物联网Void 接本地 LM Studio 模型实战:一套本地 AI 代码编辑工作流
Void 接本地 LM Studio 模型实战:一套本地 AI 代码编辑工作流 Void 是一款开源 AI 代码编辑器,可以看作 Cursor 的本地化替代方案
代码编辑器开发工具AI Agent人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考