How to create an instance of enum using reflection

2020-02-05 02:51发布

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. This means we cannot create an instance of enum using reflection, right?

I have done some tests, with an enum class here:

public enum Weekday {}

Then I tried to create an instance of 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. I want to know why. Thank you.

6条回答
孤傲高冷的网名
2楼-- · 2020-02-05 03:18

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.

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:

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; - in Class.class, note that it can be null - java will regenerate them on next usage.
private volatile transient Map<String, T> enumConstantDirectory = null; - in Class.class, note that it can be null too, same as field above.

So just set them to null using reflections and your new value is ready to use.
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:

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. I used javassist library to reduce code needed to generate new class:

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.

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

查看更多
Luminary・发光体
3楼-- · 2020-02-05 03:19

Enums has been designed to be treated as constant objects. It overrides readObject and throws invalid object exception to prevent default serialization. Also it overrides clone() and throws clone not supported exception. As far as reflection is concerned, the constructor of Enum is protected.So if you use above code it will throw NoSuchMethodFound.

Even if you use getDeclaredConstructor() instead of getConstructor, you should get the same exception. I assume its been restricted through SecurityManager in java.

查看更多
成全新的幸福
4楼-- · 2020-02-05 03:20

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:

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.

查看更多
Explosion°爆炸
5楼-- · 2020-02-05 03:29

This is built into the language. From the Java Language Specification (§8.9):

It is a compile-time error to attempt to explicitly instantiate an enum type (§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. 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.

EDIT: See the answer by @GotoFinal for how to break this "guarantee" using reflection.

查看更多
我欲成王,谁敢阻挡
6楼-- · 2020-02-05 03:29

So if your objective is to persistent and then reconstructed the enum information. You will need to persist the enumClassName and its value.

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);
查看更多
放我归山
7楼-- · 2020-02-05 03:39

This may be reviving a dead post, but you can get an instance of every constant declared using Weekday.class.getEnumConstants(). This returns an array of all the constatants, where getting a single instance is trivial, getEnumConstants()[0].

查看更多
登录 后发表回答