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

时间:2012-03-08 07:37:01

标签: java reflection enums

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

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

public enum Weekday {}

然后我尝试创建Weekday的实例:

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

如你所知,它不起作用。当我将关键字enum更改为class时,它可以正常工作。我想知道为什么。谢谢。

6 个答案:

答案 0 :(得分:18)

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

  

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

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

编辑:请参阅the answer by @GotoFinal了解如何使用反射打破此“保证”。

答案 1 :(得分:10)

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

类似于此示例枚举:

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()将返回有效列表,我们可以通过使用好的旧技巧更改final字段的值来再次使final字段变为非final来完成此操作:

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);

还有另一个存储枚举常量的字段,因此也必须执行类似的操作,这一点很重要: private volatile transient T[] enumConstants = null;-在Class.class中,请注意它可以为null-Java将在下次使用时重新生成它们。
private volatile transient Map<String, T> enumConstantDirectory = null;-在Class.class中,请注意,它也可以为null,与上面的字段相同。

因此,只需使用反射将它们设置为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

答案 2 :(得分:6)

这可能会重新发布死信,但您可以使用Weekday.class.getEnumConstants()获取声明的每个常量的实例。这将返回所有常量的数组,其中获取单个实例是微不足道的getEnumConstants()[0]

答案 3 :(得分:1)

正确的是,枚举类的新实例不能反复创建,甚至不能用反射创建。

以下代码演示了这一点:

val weekdayClass = classOf[Weekday]
val weekdayConstructor = weekdayClass getDeclaredConstructor (classOf[String], classOf[Int])
weekdayConstructor setAccessible true
weekdayConstructor newInstance ("", Integer.valueOf(0))

通常,这应该有效。但就枚举而言,这是Constructor#newInstance中的特殊内容:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

因此,在尝试实例化新的枚举实例时,我们收到以下异常:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        at java.lang.reflect.Constructor.newInstance(Constructor.java:520)
        ...

我认为最后一种方法(可能会成功,因为没有运行检查或构造函数)涉及sun.misc.Unsafe#allocateInstance

答案 4 :(得分:1)

因此,如果您的目标是持久化,然后重建枚举信息。您需要保留enumClassName及其值。

public enum DaysOfWeek{ Mon, Tue, Wed, Thu, Fri, Sat, Sun }

DaysOfWeek dow = DaysOfWeek.Tue;
String value = dow.toString();
String enumClassName = dow.getClass().getName();

// Persist value and enumClassName
// ...

// Reconstitute the data 
Class clz = Class.forName(enumClassName);
Object o = Enum.valueOf(clz, value);
DaysOfWeek dow2 = (DaysOfWeek)o;
System.out.println(dow2);

答案 5 :(得分:0)

枚举被设计为被视为常量对象。它会覆盖readObject并抛出无效对象异常以防止默认序列化。它还会覆盖clone()并抛出克隆不支持的异常。就反射而言,Enum的构造函数是受保护的。所以如果使用上面的代码,它将抛出NoSuchMethodFound。

即使使用getDeclaredConstructor()而不是getConstructor,也应该得到相同的异常。我认为它是通过java中的SecurityManager限制的。