Как создать экземпляр enum с использованием отражения в Java? - PullRequest
12 голосов
/ 08 марта 2012

Когда я читаю 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 ]

18 голосов
/ 08 марта 2012

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

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

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

РЕДАКТИРОВАТЬ: См. ответ @ GotoFinal о том, как сломать эту «гарантию» с помощью отражения.

10 голосов
/ 09 июля 2018

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

Как в этом примере 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, так что Enum.values ​​() вернет действительный список, мы можем сделать это, изменив значение поля 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 обратите внимание, что оно может быть нулевым - 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");

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

Источник: https://blog.gotofinal.com/java/diorite/breakingjava/2017/06/24/dynamic-enum.html

8 голосов
/ 10 июля 2015

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

1 голос
/ 24 июля 2014

Так что, если ваша цель состоит в том, чтобы сохранить, а затем восстановить информацию enum. Вам нужно будет сохранить 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);
1 голос
/ 21 января 2013

Правильно, что новые экземпляры класса 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.

0 голосов
/ 08 марта 2012

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

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

...