簡體   English   中英

如何在Java中使用反射創建枚舉實例?

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

當我閱讀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 ,它可以工作。 我想知道為什么。 謝謝。

這是內置於語言中的。 Java語言規范(第8.9節)

嘗試顯式實例化枚舉類型(第15.9.1節)是編譯時錯誤。 Enum中的最終克隆方法確保永遠不會克隆枚舉常量,並且序列化機制的特殊處理可確保不會因反序列化而創建重復實例。 禁止對枚舉類型進行反射實例化。 總之,這四件事確保除了枚舉常量定義的實例之外不存在枚舉類型的實例。

這樣做的全部目的是允許安全地使用==來比較Enum實例。

編輯:請參閱@GotoFinal的答案,了解如何使用反射打破這種“保證”。

可以在運行時創建新的枚舉實例 - 但這是一個非常糟糕的主意,可能會在任何更新中中斷。 您可以使用不安全或反射。

就像在這個例子枚舉:

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

還有另一個存儲枚舉常量的字段,因此對它執行類似的技巧也很重要: 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");

請注意,您還需要初始化內部枚舉字段。
同樣使用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.注意,這不可能枚舉不包含子類的類 - 因此沒有任何重寫方法,因為枚舉被聲明為最終類。

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

這可能會重新發布死帖,但您可以使用Weekday.class.getEnumConstants()獲取聲明的每個常量的實例。 這將返回所有常量的數組,其中獲取單個實例是微不足道的, getEnumConstants()[0]

正確的是,枚舉類的新實例不能反復創建,甚至不能用反射創建。

以下代碼演示了這一點:

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

通常,這應該工作。 但是對於枚舉,這在Constructor#newInstance #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

因此,如果您的目標是持久化,然后重建枚舉信息。 您需要保留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);

枚舉被設計為被視為常量對象。 它會覆蓋readObject並拋出無效對象異常以防止默認序列化。 它還會覆蓋clone()並拋出克隆不支持的異常。 就反射而言,Enum的構造函數受到保護。因此,如果使用上面的代碼,它將拋出NoSuchMethodFound。

即使使用getDeclaredConstructor()而不是getConstructor,也應該得到相同的異常。 我認為它是通過java中的SecurityManager限制的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM