如何在Java中使用反射创建枚举的实例?

2022-09-03 03:29:04

当我阅读 Effective Java 时,作者告诉我,单元素类型是实现单例的最佳方式,因为我们不必考虑复杂的序列化或反射攻击。这意味着我们不能创建一个使用反射的实例,对吧?enumenum

我做了一些测试,这里有一个类:enum

public enum Weekday {}

然后我尝试创建一个实例:Weekday

Class<Weekday> weekdayClass = Weekday.class;
Constructor<Weekday> cw = weekdayClass.getConstructor(null);
cw.setAccessible(true);
cw.newInstance(null);

如您所知,它不起作用。当我将关键字更改为 时,它有效。我想知道为什么。谢谢。enumclass


答案 1

这是内置于语言中的。来自 Java 语言规范 (§8.9)

尝试显式实例化枚举类型 (§15.9.1) 是编译时错误。Enum 中的 final clone 方法可确保永远无法克隆 enum 常量,并且序列化机制的特殊处理可确保永远不会因反序列化而创建重复的实例。禁止枚举类型的反射实例化。总之,这四件事可确保除了枚举常量定义的实例之外,不存在枚举类型的实例。

这样做的整个目的是允许安全地使用比较实例。==Enum

编辑:通过@GotoFinal查看答案,了解如何使用反射来打破这种“保证”。


答案 2

可以在运行时创建新的枚举实例 - 但这是一个非常糟糕的主意,可能会在任何更新中中断。为此,您可以使用不安全或反射。

就像这个例子枚举一样:

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.classprivate 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