Java 中的字节码模式:使用自定义虚拟机解释指令
字节码设计模式的意图
Java 中的字节码设计模式允许将行为编码为虚拟机的指令,使其成为游戏开发和其他应用程序中强大的工具。
字节码模式的详细解释以及实际例子
现实世界的例子
字节码设计模式在现实世界中的一个类似例子可以从将一本图书翻译成多种语言的过程中看到。与其直接将图书从原文翻译成其他每种语言,不如先将图书翻译成一种通用的中间语言,比如世界语。这种中间版本更容易翻译,因为它更简单、结构更清晰。每种目标语言的翻译人员然后将世界语翻译成各自的语言。这种方法可以确保一致性,减少错误,并简化翻译过程,类似于字节码如何充当中间表示形式来优化和简化跨不同平台的高级编程语言的执行。
通俗易懂地说
字节码模式允许通过数据而不是代码来驱动行为。
gameprogrammingpatterns.com 文档指出
指令集定义了可以执行的低级操作。一系列指令被编码为字节序列。虚拟机一次执行这些指令,使用一个堆栈来存储中间值。通过组合指令,可以定义复杂的高级行为。
Java 中字节码模式的编程示例
在这个编程示例中,我们展示了 Java 中的字节码模式如何通过一组定义良好的操作来简化复杂虚拟机指令的执行。这个现实世界的例子演示了 Java 中的字节码设计模式如何通过允许巫师的行为通过字节码指令轻松调整来简化游戏编程。
一个团队正在开发一款新游戏,游戏中巫师互相战斗。巫师的行为需要经过数百次的玩测试才能仔细调整和迭代。每次游戏设计师想要改变行为时都要求程序员修改代码并不是最佳选择,因此巫师的行为是作为数据驱动的虚拟机实现的。
其中一个最重要的游戏对象是 Wizard
类。
@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++;
}
}
接下来,我们展示了虚拟机的可用指令。每个指令都有其自己的语义,说明它是如何与堆栈数据交互的。例如,ADD 指令从堆栈中获取最上面的两个项目,将它们加在一起并将结果推送到堆栈中。
@AllArgsConstructor
@Getter
public enum Instruction {
LITERAL(1), // e.g. "LITERAL 0", push 0 to stack
SET_HEALTH(2), // e.g. "SET_HEALTH", pop health and wizard number, call set health
SET_WISDOM(3), // e.g. "SET_WISDOM", pop wisdom and wizard number, call set wisdom
SET_AGILITY(4), // e.g. "SET_AGILITY", pop agility and wizard number, call set agility
PLAY_SOUND(5), // e.g. "PLAY_SOUND", pop value as wizard number, call play sound
SPAWN_PARTICLES(6), // e.g. "SPAWN_PARTICLES", pop value as wizard number, call spawn particles
GET_HEALTH(7), // e.g. "GET_HEALTH", pop value as wizard number, push wizard's health
GET_AGILITY(8), // e.g. "GET_AGILITY", pop value as wizard number, push wizard's agility
GET_WISDOM(9), // e.g. "GET_WISDOM", pop value as wizard number, push wizard's wisdom
ADD(10), // e.g. "ADD", pop 2 values, push their sum
DIVIDE(11); // e.g. "DIVIDE", pop 2 values, push their division
// Other properties and methods...
}
我们示例的核心是 VirtualMachine
类。它以指令作为输入并执行它们以提供游戏对象的 behaviour。
@Getter
@Slf4j
public class VirtualMachine {
private final Stack<Integer> stack = new Stack<>();
private final Wizard[] wizards = new Wizard[2];
public VirtualMachine() {
wizards[0] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
wizards[1] = new Wizard(randomInt(3, 32), randomInt(3, 32), randomInt(3, 32),
0, 0);
}
public VirtualMachine(Wizard wizard1, Wizard wizard2) {
wizards[0] = wizard1;
wizards[1] = wizard2;
}
public void execute(int[] bytecode) {
for (var i = 0; i < bytecode.length; i++) {
Instruction instruction = Instruction.getInstruction(bytecode[i]);
switch (instruction) {
case LITERAL:
// Read the next byte from the bytecode.
int value = bytecode[++i];
// Push the next value to stack
stack.push(value);
break;
case SET_AGILITY:
var amount = stack.pop();
var wizard = stack.pop();
setAgility(wizard, amount);
break;
case SET_WISDOM:
amount = stack.pop();
wizard = stack.pop();
setWisdom(wizard, amount);
break;
case SET_HEALTH:
amount = stack.pop();
wizard = stack.pop();
setHealth(wizard, amount);
break;
case GET_HEALTH:
wizard = stack.pop();
stack.push(getHealth(wizard));
break;
case GET_AGILITY:
wizard = stack.pop();
stack.push(getAgility(wizard));
break;
case GET_WISDOM:
wizard = stack.pop();
stack.push(getWisdom(wizard));
break;
case ADD:
var a = stack.pop();
var b = stack.pop();
stack.push(a + b);
break;
case DIVIDE:
a = stack.pop();
b = stack.pop();
stack.push(b / a);
break;
case PLAY_SOUND:
wizard = stack.pop();
getWizards()[wizard].playSound();
break;
case SPAWN_PARTICLES:
wizard = stack.pop();
getWizards()[wizard].spawnParticles();
break;
default:
throw new IllegalArgumentException("Invalid instruction value");
}
LOGGER.info("Executed " + instruction.name() + ", Stack contains " + getStack());
}
}
public void setHealth(int wizard, int amount) {
wizards[wizard].setHealth(amount);
}
// Other properties and methods...
}
现在我们可以展示利用虚拟机的完整示例。
public static void main(String[] args) {
var vm = new VirtualMachine(
new Wizard(45, 7, 11, 0, 0),
new Wizard(36, 18, 8, 0, 0));
vm.execute(InstructionConverterUtil.convertToByteCode(LITERAL_0));
vm.execute(InstructionConverterUtil.convertToByteCode(LITERAL_0));
vm.execute(InstructionConverterUtil.convertToByteCode(String.format(HEALTH_PATTERN, "GET")));
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(String.format(HEALTH_PATTERN, "SET")));
}
以下是控制台输出。
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 []
在 Java 中利用字节码设计模式可以显著提高基于虚拟机的应用程序的灵活性和可维护性。
何时在 Java 中使用字节码模式
当你需要定义大量行为,并且游戏的实现语言不适合时,使用字节码模式。
- 它太低级了,导致编程变得枯燥乏味或容易出错。
- 由于编译时间过长或其他工具问题,迭代它需要很长时间。
- 它有太多信任。如果你想要确保定义的行为不会破坏游戏,你需要将其与代码库的其余部分隔离。
Java 中字节码模式的实际应用
- Java 虚拟机 (JVM) 使用字节码来允许 Java 程序运行在任何安装了 JVM 的设备上。
- Python 将其脚本编译成字节码,然后由 Python 虚拟机解释。
- .NET Framework 使用一种称为 Microsoft 中间语言 (MSIL) 的字节码形式。
字节码模式的优点和权衡
优点
- 可移植性:程序可以在任何具有兼容 VM 的平台上运行。
- 安全性:VM 可以对字节码执行安全检查。
- 性能:JIT 编译器可以在运行时优化字节码,与解释执行的代码相比,可能会提高性能。
权衡
- 开销:运行字节码通常比运行本地代码有更多开销,这可能会影响性能。
- 复杂性:实现和维护 VM 会增加系统的复杂性。