简体   繁体   English

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

[英]How to create an instance of enum using reflection in Java?

When I'm reading Effective Java , the author told me that a single-element enum type is the best way to implement a singleton, because we don't have to consider sophisticated serialization or reflection attacks. 当我阅读Effective Java时 ,作者告诉我单元素enum类型是实现单例的最佳方式,因为我们不必考虑复杂的序列化或反射攻击。 This means we cannot create an instance of enum using reflection, right? 这意味着我们无法使用反射创建enum实例,对吧?

I have done some tests, with an enum class here: 我做了一些测试,这里有一个enum类:

public enum Weekday {}

Then I tried to create an instance of Weekday : 然后我尝试创建一个Weekday的实例:

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

As you know, it doesn't work. 如你所知,它不起作用。 When I change the key word enum to class , it works. 当我将关键词enum更改为class ,它可以工作。 I want to know why. 我想知道为什么。 Thank you. 谢谢。

This is built into the language. 这是内置于语言中的。 From the Java Language Specification (§8.9) : Java语言规范(第8.9节)

It is a compile-time error to attempt to explicitly instantiate an enum type (§15.9.1). 尝试显式实例化枚举类型(第15.9.1节)是编译时错误。 The final clone method in Enum ensures that enum constants can never be cloned, and the special treatment by the serialization mechanism ensures that duplicate instances are never created as a result of deserialization. Enum中的最终克隆方法确保永远不会克隆枚举常量,并且序列化机制的特殊处理可确保不会因反序列化而创建重复实例。 Reflective instantiation of enum types is prohibited. 禁止对枚举类型进行反射实例化。 Together, these four things ensure that no instances of an enum type exist beyond those defined by the enum constants. 总之,这四件事确保除了枚举常量定义的实例之外不存在枚举类型的实例。

The whole purpose of this is to allow the safe use of == to compare Enum instances. 这样做的全部目的是允许安全地使用==来比较Enum实例。

EDIT: See the answer by @GotoFinal for how to break this "guarantee" using reflection. 编辑:请参阅@GotoFinal的答案,了解如何使用反射打破这种“保证”。

It is possible to create new enum instance in runtime - but it is very bad idea and might break in any update. 可以在运行时创建新的枚举实例 - 但这是一个非常糟糕的主意,可能会在任何更新中中断。 You can use unsafe or reflections for this. 您可以使用不安全或反射。

Like at this example enum: 就像在这个例子枚举:

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

We can use 我们可以用

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

On java 9 this might not compile due to usage of internal class as I described that in comment - you can skip that using unsafe or even more reflections. 在java 9上,由于我在评论中描述的内部类的使用,这可能无法编译 - 你可以使用不安全甚至更多的反射来跳过它。

But then we also need to add that constant to enum itself, so Enum.values() will return valid list, we can do this by changing value of final field using good old trick to make final field non-final again: 但是我们还需要添加该常量以枚举自身,因此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);
}

And then just change that field to new value that include our new field: 然后只需将该字段更改为包含新字段的新值:

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

There is also another field that store enum constant, so it is important to do similar trick to it too: private volatile transient T[] enumConstants = null; 还有另一个存储枚举常量的字段,因此对它执行类似的技巧也很重要: private volatile transient T[] enumConstants = null; - in Class.class , note that it can be null - java will regenerate them on next usage. - 在Class.class ,请注意它可以为null - java将在下次使用时重新生成它们。
private volatile transient Map<String, T> enumConstantDirectory = null; - in Class.class , note that it can be null too, same as field above. - 在Class.class ,请注意它也可以为null,与上面的字段相同。

So just set them to null using reflections and your new value is ready to use. 因此,只需使用反射将它们设置为null,即可使用新值。
The only impossible thing without editing class using instrumentation or other tricks is to add real field to that enum for our new value. 没有使用仪器或其他技巧编辑类的唯一不可能的事情是为我们的新值添加真实字段到该枚举。

Also it is possible to create new enum instance using Unsafe class: 也可以使用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);
}

But unsafe class does not call the constructor, so you need to init all fields manually... 但是不安全的类不会调用构造函数,因此您需要手动初始化所​​有字段...

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

Note that you also need to initialize internal enum fields. 请注意,您还需要初始化内部枚举字段。
Also using unsafe it should be possible to declare new class to create new instance of abstract enum classes. 同样使用unsafe,应该可以声明新类来创建抽象枚举类的新实例。 I used javassist library to reduce code needed to generate new class: 我使用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();
}

This will print 5 and then 3. Note that this is impossible to enum classes that does not contain subclasses - so without any overriden methods, as then enum is declared as final class. 这将打印5然后打印3.注意,这不可能枚举不包含子类的类 - 因此没有任何重写方法,因为枚举被声明为最终类。

Source: https://blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/dynamic-enum.html 来源: https//blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/dynamic-enum.html

This may be reviving a dead post, but you can get an instance of every constant declared using Weekday.class.getEnumConstants() . 这可能会重新发布死帖,但您可以使用Weekday.class.getEnumConstants()获取声明的每个常量的实例。 This returns an array of all the constatants, where getting a single instance is trivial, getEnumConstants()[0] . 这将返回所有常量的数组,其中获取单个实例是微不足道的, getEnumConstants()[0]

It is correct that new instances of an enum class cannot be created retro-actively, not even with reflection. 正确的是,枚举类的新实例不能反复创建,甚至不能用反射创建。

The following code demonstrates this: 以下代码演示了这一点:

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

Usually, this should work. 通常,这应该工作。 But in the case of enums, this is special-cased in Constructor#newInstance : 但是对于枚举,这在Constructor#newInstance #newInstance中是特殊的:

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

Thus, we receive the following exception when trying to instantiate a new enum instance: 因此,在尝试实例化新的枚举实例时,我们收到以下异常:

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

I assume that the last approach (which will probably be successful, because no checks or constructors are run) involves sun.misc.Unsafe#allocateInstance . 我假设最后一种方法(可能会成功,因为没有运行检查或构造函数)涉及sun.misc.Unsafe#allocateInstance

So if your objective is to persistent and then reconstructed the enum information. 因此,如果您的目标是持久化,然后重建枚举信息。 You will need to persist the enumClassName and its value. 您需要保留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);

Enums has been designed to be treated as constant objects. 枚举被设计为被视为常量对象。 It overrides readObject and throws invalid object exception to prevent default serialization. 它会覆盖readObject并抛出无效对象异常以防止默认序列化。 Also it overrides clone() and throws clone not supported exception. 它还会覆盖clone()并抛出克隆不支持的异常。 As far as reflection is concerned, the constructor of Enum is protected.So if you use above code it will throw NoSuchMethodFound. 就反射而言,Enum的构造函数受到保护。因此,如果使用上面的代码,它将抛出NoSuchMethodFound。

Even if you use getDeclaredConstructor() instead of getConstructor, you should get the same exception. 即使使用getDeclaredConstructor()而不是getConstructor,也应该得到相同的异常。 I assume its been restricted through SecurityManager in java. 我认为它是通过java中的SecurityManager限制的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM