Как создать экземпляр enum, используя отражение в Java?

Когда я читаю "Эффективная 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, он работает. Я хочу знать, почему. Спасибо.

Ответ 1

Это встроено в язык. Из спецификации языка Java (§8.9):

Ошибка компиляции, чтобы попытаться явно создать экземпляр типа перечисления (§15.9.1). Последний метод clone в Enum гарантирует, что константные континуумы никогда не будут клонированы, а специальное обращение с помощью механизма сериализации гарантирует, что дублирующие экземпляры никогда не создаются в результате десериализации. Отражающее создание типов перечислений запрещено. Вместе эти четыре вещи гарантируют, что не существует экземпляров типа enum, кроме тех, которые определены константами перечисления.

Вся цель этого - обеспечить безопасное использование == для сравнения экземпляров Enum.

EDIT: см. Ответ @GotoFinal о том, как разбить эту "гарантию" с помощью отражения.

Ответ 2

В runtime можно создать новый экземпляр 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); }
    }
}

Мы можем использовать

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

Существует также другое поле, в котором хранится enum constant, поэтому важно также сделать для него подобный трюк: private volatile transient T[] enumConstants = null; - в Class.class обратите внимание, что он может быть null - java будет восстанавливать их при следующем использовании.
private volatile transient Map<String, T> enumConstantDirectory = null; - в Class.class обратите внимание, что он также может быть нулевым, как и поле выше.

Поэтому просто установите для них значение null, используя отражения, и ваше новое значение будет готово к использованию.
Единственное невозможное без редактирования класса с использованием инструментария или других трюков - добавить реальное поле к этому перечислению для нашего нового значения.

Также возможно создать новый экземпляр enum с использованием класса 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");

Обратите внимание, что вам также необходимо инициализировать внутренние поля enum.
Также, используя небезопасное, должно быть возможно объявить новый класс для создания нового экземпляра абстрактных классов перечисления. Я использовал библиотеку 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

Ответ 3

Это может оживить мертвую запись, но вы можете получить экземпляр каждой константы, объявленной с помощью Weekday.class.getEnumConstants(). Это возвращает массив всех констант, где получение одного экземпляра тривиально, getEnumConstants()[0].

Ответ 4

Верно, что новые экземпляры класса enum не могут быть созданы ретроактивно, даже не с отражением.

Следующий код демонстрирует это:

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

Таким образом, мы получаем следующее исключение при попытке создать экземпляр нового экземпляра enum:

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

Я предполагаю, что последний подход (который, вероятно, будет успешным, потому что не выполняется никаких проверок или конструкторов) включает sun.misc.Unsafe#allocateInstance.

Ответ 5

Итак, если ваша цель - постоянная, а затем реконструированная информация об перечислении. Вам нужно будет сохранить имя 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);

Ответ 6

Enums был разработан для обработки как постоянных объектов. Он переопределяет readObject и выдает недопустимое исключение объекта для предотвращения сериализации по умолчанию. Также он отменяет clone() и генерирует исключение clone not supported. Что касается размышлений, конструктор Enum защищен. Поэтому, если вы используете код выше, он будет вызывать NoSuchMethodFound.

Даже если вы используете getDeclaredConstructor() вместо getConstructor, вы должны получить то же исключение. Я предполагаю, что он был ограничен с помощью SecurityManager в java.