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

当我阅读Effective Java时 ,作者告诉我单元素enum类型是实现单例的最佳方式,因为我们不必考虑复杂的序列化或reflection攻击。 这意味着我们无法使用reflection创建enum实例,对吧?

我做了一些测试,这里有一个enum类:

 public enum Weekday {} 

然后我尝试创建一个Weekday的实例:

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

如你所知,它不起作用。 当我将关键词enum更改为class ,它可以工作。 我想知道为什么。 谢谢。

这是内置于语言中的。 从Java语言规范(第8.9节) :

尝试显式实例化枚举类型(第15.9.1节)是编译时错误。 Enum中的最终克隆方法确保永远不会克隆枚举常量,并且序列化机制的特殊处理可确保不会因反序列化而创建重复实例。 禁止对枚举类型进行reflection实例化。 总之,这四件事确保除了枚举常量定义的实例之外不存在枚举类型的实例。

这样做的全部目的是允许安全使用==来比较Enum实例。

编辑:请参阅@GotoFinal的答案,了解如何使用reflection打破这种“保证”。

可以在运行时创建新的枚举实例 – 但这是一个非常糟糕的主意,可能会在任何更新中中断。 您可以使用不安全或reflection。

就像在这个例子枚举:

 public enum Monster { ZOMBIE(Zombie.class, "zombie"), ORK(Ork.class, "ork"), WOLF(Wolf.class, "wolf"); private final Class entityClass; private final String entityId; Monster(Class entityClass, String entityId) { this.entityClass = entityClass; this.entityId = "monster:" + entityId; } public Class 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 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上,由于我在评论中描述的内部类的使用,这可能无法编译 – 您可以使用不安全甚至更多的reflection来跳过它。

但是我们还需要将该常量添加到枚举本身,因此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 enumConstantDirectory = null; – 在Class.class ,请注意它也可以为null,与上面的字段相同。

因此,只需使用reflection将它们设置为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

这可能会使死亡post恢复,但您可以使用Weekday.class.getEnumConstants()获取声明的每个常量的实例。 这将返回所有常量的数组,其中获取单个实例是微不足道的, getEnumConstants()[0]

正确的是,枚举类的新实例不能反复创建,甚至不能用reflection创建。

以下代码演示了这一点:

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

因此,在尝试实例化新的枚举实例时,我们收到以下exception:

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

我假设最后一种方法(可能会成功,因为没有运行检查或构造函数)涉及sun.misc.Unsafe#allocateInstance

枚举被设计为被视为常量对象。 它会覆盖readObject并抛出无效对象exception以防止默认序列化。 它还会覆盖clone()并抛出克隆不支持的exception。 就reflection而言,Enum的构造函数受到保护。因此,如果使用上面的代码,它将抛出NoSuchMethodFound。

即使您使用getDeclaredConstructor()而不是getConstructor,您也应该获得相同的exception。 我假设它已经通过java中的SecurityManager进行了限制。

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