可以在运行时创建新的枚举实例 - 但这是一个非常糟糕的主意,可能会在任何更新中中断。为此,您可以使用不安全或反射。
就像这个例子枚举一样:
public enum Monster {
ZOMBIE(Zombie.class, "zombie"),
ORK(Ork.class, "ork"),
WOLF(Wolf.class, "wolf");
private final Class<? extends Entity> entityClass;
private final String entityId;
Monster(Class<? extends Entity> entityClass, String entityId) {
this.entityClass = entityClass;
this.entityId = "monster:" + entityId;
}
public Class<? extends Entity> getEntityClass() { return this.entityClass; }
public String getEntityId() { return this.entityId; }
public Entity create() {
try { return entityClass.newInstance(); }
catch (InstantiationException | IllegalAccessException e) { throw new InternalError(e); }
}
}
我们可以使用
Class<Monster> monsterClass = Monster.class;
// first we need to find our constructor, and make it accessible
Constructor<?> constructor = monsterClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
// this is this same code as in constructor.newInstance, but we just skipped all that useless enum checks ;)
Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
constructorAccessorField.setAccessible(true);
// sun.reflect.ConstructorAccessor -> internal class, we should not use it, if you need use it, it would be better to actually not import it, but use it only via reflections. (as package may change, and will in java 9+)
ConstructorAccessor ca = (ConstructorAccessor) constructorAccessorField.get(constructor);
if (ca == null) {
Method acquireConstructorAccessorMethod = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
acquireConstructorAccessorMethod.setAccessible(true);
ca = (ConstructorAccessor) acquireConstructorAccessorMethod.invoke(constructor);
}
// note that real constructor contains 2 additional parameters, name and ordinal
Monster enumValue = (Monster) ca.newInstance(new Object[]{"CAERBANNOG_RABBIT", 4, CaerbannogRabbit.class, "caerbannograbbit"});// you can call that using reflections too, reflecting reflections are best part of java ;)
在java 9上,由于使用了内部类,这可能无法编译,正如我在注释中描述的那样 - 您可以使用不安全甚至更多的反射来跳过它。
但是,我们还需要将该常量添加到枚举本身,因此Enum.values()将返回有效的列表,我们可以通过使用良好的旧技巧更改最终字段的值来使最终字段再次成为非最终字段来做到这一点:
static void makeAccessible(Field field) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~ Modifier.FINAL);
}
然后只需将该字段更改为包含我们的新字段的新值:
Field valuesField = Monster.class.getDeclaredField("$VALUES");
makeAccessible(valuesField);
// just copy old values to new array and add our new field.
Monster[] oldValues = (Monster[]) valuesField.get(null);
Monster[] newValues = new Monster[oldValues.length + 1];
System.arraycopy(oldValues, 0, newValues, 0, oldValues.length);
newValues[oldValues.length] = enumValue;
valuesField.set(null, newValues);
还有另一个存储枚举常量的字段,因此执行与它类似的技巧也很重要: - 在 中,请注意它可以是空的 - java将在下次使用时重新生成它们。
- 在 中,请注意,它也可以为空,与上面的字段相同。private volatile transient T[] enumConstants = null;
Class.class
private volatile transient Map<String, T> enumConstantDirectory = null;
Class.class
因此,只需使用反射将它们设置为 null,您的新值就可以使用了。
如果不使用检测或其他技巧编辑类,唯一不可能的事情就是将真实字段添加到该枚举中,以获得我们的新值。
也可以使用 Unsafe 类创建新的枚举实例:
public static void unsafeWay() throws Throwable {
Constructor<?> constructor = Unsafe.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Unsafe unsafe = (Unsafe) constructor.newInstance();
Monster enumValue = (Monster) unsafe.allocateInstance(Monster.class);
}
但是不安全类不调用构造函数,所以你需要手动初始化所有字段...
Field ordinalField = Enum.class.getDeclaredField("ordinal");
makeAccessible(ordinalField);
ordinalField.setInt(enumValue, 5);
Field nameField = Enum.class.getDeclaredField("name");
makeAccessible(nameField);
nameField.set(enumValue, "LION");
Field entityClassField = Monster.class.getDeclaredField("entityClass");
makeAccessible(entityClassField);
entityClassField.set(enumValue, Lion.class);
Field entityIdField = Monster.class.getDeclaredField("entityId");
makeAccessible(entityIdField);
entityIdField.set(enumValue, "Lion");
请注意,您还需要初始化内部枚举字段。
同样使用不安全,应该可以声明新类以创建抽象枚举类的新实例。我使用javassist库来减少生成新类所需的代码:
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(MyEnum.VALUE.getSomething());
ClassPool classPool = ClassPool.getDefault();
CtClass enumCtClass = classPool.getCtClass(MyEnum.class.getName());
CtClass ctClass = classPool.makeClass("com.example.demo.MyEnum$2", enumCtClass);
CtMethod getSomethingCtMethod = new CtMethod(CtClass.intType, "getSomething", new CtClass[0], ctClass);
getSomethingCtMethod.setBody("{return 3;}");
ctClass.addMethod(getSomethingCtMethod);
Constructor<?> unsafeConstructor = Unsafe.class.getDeclaredConstructors()[0];
unsafeConstructor.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeConstructor.newInstance();
MyEnum newInstance = (MyEnum) unsafe.allocateInstance(ctClass.toClass());
Field singletonInstance = MyEnum.class.getDeclaredField("VALUE");
makeAccessible(singletonInstance);
singletonInstance.set(null, newInstance);
System.out.println(MyEnum.VALUE.getSomething());
}
static void makeAccessible(Field field) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~ Modifier.FINAL);
}
}
enum MyEnum {
VALUE {
@Override
public int getSomething() {
return 5;
}
};
public abstract int getSomething();
}
这将打印 5,然后打印 3。请注意,这不可能枚举不包含子类的类 - 因此没有任何重写的方法,因为枚举被声明为最终类。
资料来源:https://blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/dynamic-enum.html