PAYATHON 2026

Java 模块化 RPG 系统应使用数组、对象还是反射管理数据?

数据小许

结论

建议把同一个 RPG 系统的数据放在一个对象中,再用 List<RPGSystem> 或按标识查找的 Map 管理。第三方系统需要统一的访问方式,或需要实现各自的行为时,可以定义 interface。

仅为了节省 RAM 而保存类名字符串,再通过 reflection 读取字段,没有充分依据。通过反射访问时,相关类和数据仍然需要进入内存;字段名称、类型和访问权限方面的问题也可能更晚才暴露。

当前数组方案可以工作,但多组数组依赖索引对应,反复扩容数组的方式也值得调整。

当前代码是否重复保存了数据?

需要区分基本类型的值和对象引用。

你的注册方法中:

descriptions.add(description);
expLists.add(expList);

这两行保存的是已有对象的引用,不会复制整个 List<String> 或整个 int[]。系统类中的字段和注册类中的集合可以指向同一个对象。

String 也是对象。把 name 放进 String[] 保存的是引用,不会因此复制字符串内容。

startingLevel 和 maxLevel 则是 int,注册时会把数值复制到相应的 int[] 中。这会增加存储,但仅凭这段代码无法判断是否会形成实际的内存瓶颈。还需要知道系统数量、经验表长度和其他运行数据的规模。

另外:

rpgSystemsList = Arrays.asList(rpgSystems);

得到的是由该数组支持、大小固定的列表,字符串数据没有被深拷贝。如果之后将 rpgSystems 替换为新数组,之前取得的列表仍然关联旧数组,不会自动切换过去。

这些数据都会占用运行时内存。代码没有展示文件或数据库写入,这里讨论的是内存中的数据组织,还不涉及重启后恢复数据的持久化。

为什么 reflection 不能直接解决内存问题?

类名字符串只是一种定位类的方式,不会自动让类的字段不再占用内存。

如果通过 reflection 读取某个类的静态字段,通常仍然需要加载并初始化相关类。原来的 static 字段也不会因为采用反射访问就消失。若访问的是实例字段,还需要有对应实例。

只保存类名并推迟加载,可能推迟部分内存分配。是否有收益取决于哪些系统实际会被使用,不能由此得出反射读取字段本身更省内存的结论。

对于已知的 RPG 系统扩展协议,直接调用接口方法通常更容易维护:

int maxLevel = system.getMaxLevel();

编译器可以检查方法是否存在、返回类型是否正确。通过字段名反射访问时,这些检查通常要等到运行时才能完成。

如果插件发现过程确实需要 reflection,可以在发现或创建实例时使用,再把实例注册为统一接口类型。后续读取等级、描述和经验表时就不必每次都依赖反射。

把同一个系统的数据放进同一个对象

当前注册类把一条逻辑记录拆开放在多组数组和列表中,要求它们的长度和顺序始终一致。例如,删除一个系统时,如果遗漏了其中一个列表,名称和等级就可能对应到不同系统。

改用对象集合后,每个对象包含自己的名称、描述、等级范围和经验表,注册类只负责保存系统对象。

下面是一个最小示例。代码沿用原问题中的 List.of,需要 Java 9 或更高版本;示例未包含具体 Minecraft mod 加载器的生命周期接入。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public interface RPGSystem {
    String getName();
    List<String> getDescription();
    int getStartingLevel();
    int getMaxLevel();
    int[] getExpList();
}

final class GenericRPGSystem implements RPGSystem {
    private final List<String> description =
            List.of("rpgsystem.grpg.desc1");

    private final int[] expList = {
        1000, 2000, 3000, 4000, 5000,
        6000, 7000, 8000, 9000, 10000
    };

    @Override
    public String getName() {
        return "rpgsystem.grpg.name";
    }

    @Override
    public List<String> getDescription() {
        return description;
    }

    @Override
    public int getStartingLevel() {
        return 1;
    }

    @Override
    public int getMaxLevel() {
        return 10;
    }

    @Override
    public int[] getExpList() {
        return expList.clone();
    }
}

final class RPGSystemRegister {
    private final List<RPGSystem> systems = new ArrayList<>();

    public void register(RPGSystem system) {
        systems.add(Objects.requireNonNull(system, "system"));
    }

    public List<RPGSystem> getSystems() {
        return Collections.unmodifiableList(
                new ArrayList<>(systems));
    }
}

这段代码可放在 RPGSystem.java 中,实际项目也可以拆分为独立文件。初始化时,按原来的启用条件注册:

RPGSystemRegister registry = new RPGSystemRegister();

boolean enabled = true;
if (enabled) {
    registry.register(new GenericRPGSystem());
}

for (RPGSystem system : registry.getSystems()) {
    System.out.println(system.getName());
}

这里的 interface 定义访问约定,数据保存在实现对象中。接口本身不会让这些数据自动减少内存占用。

如果各系统只是配置数值不同,没有不同的实现行为,普通数据类就够用。以后需要让不同系统采用不同的升级规则或经验计算方式时,也可以再用接口表达这些差异。

如果主要操作是按系统标识查找,可以改用 Map<String, RPGSystem>。用于查找的标识应有明确的唯一性约定;当前代码中的名称是一个文本键,不能仅凭示例就认定它一定适合作为唯一标识。

数组的使用方式可以直接改进

Java 数组长度固定。如果 ArrayUtils.addAll 每次追加都创建新数组并复制旧元素,连续追加就会产生中间数组和重复复制。逐个添加大量元素时,累计复制量可能达到 O(n²)。

经验表已经确定时,直接初始化即可:

private static final int[] EXP_LIST = {
    1000, 2000, 3000, 4000, 5000,
    6000, 7000, 8000, 9000, 10000
};

系统数量动态增长时,使用 ArrayList<RPGSystem> 更方便。经验表使用 int[] 也很合适,不必为了采用对象设计就把它改成装箱的 List<Integer>。

实现时需要注意什么?

  • 原问题没有说明经验表每项代表升到某级所需的经验,还是累计经验阈值。应先确定等级与下标的对应规则,再校验表长和等级范围。
  • 原代码直接返回内部数组和列表,调用方可以修改注册数据。示例通过复制经验数组和返回不可修改的集合快照来保护结构,但第三方系统对象自身是否可变,仍需接口约定。
  • 示例的 getExpList() 每次都返回副本,可以防止外部修改。如果经验查询非常频繁,可以提供按等级查询的方法,避免每次复制整张表。
  • 原来的 init() 每执行一次都会继续追加经验并再次注册。应根据 mod 的生命周期保证只初始化一次,或者明确实现可重复初始化的行为。
  • 对象集合更容易维持数据关系,但不保证一定比平行的基本类型数组更省内存。如果运行中出现内存压力,再根据实际分配和保留情况决定是否需要更紧凑的结构。

备注:内容仅供参考。