当我阅读Effective Java时,作者告诉我,单元素enum
类型是实现singleton的最佳方式,因为我们不必考虑复杂的序列化或反射攻击。这意味着我们不能使用反射创建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 ;)
在java9上,由于使用了内部类,这可能不会编译,正如我在评论中所描述的那样——您可以使用不安全的甚至更多的反射来跳过它。
但我们还需要将该常量添加到枚举本身,因此enum.values()将返回有效列表,我们可以通过使用好的老技巧更改final字段的值来实现这一点,使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
中,注意它可以为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);
}
但是unsafe类不调用构造函数,所以需要手动初始化所有字段。。。
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
这可能会恢复一个死帖子,但您可以获得使用Weekday.class.getEnumConstants()
声明的每个常量的实例。这将返回一个所有常量的数组,其中获得一个实例是微不足道的,getEnumConstants()[0]
。
因此,如果您的目标是持久化,然后重建枚举信息。您将需要持久化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);
即使使用反射,也不能反向主动创建枚举类的新实例,这是正确的。
以下代码演示了这一点:
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");
因此,当尝试实例化新的枚举实例时,我们会收到以下异常:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:520)
...
我假设最后一种方法(可能会成功,因为没有运行任何检查或构造函数)涉及sun.misc.Unsafe#allocateInstance
。
枚举被设计为被视为常量对象。它重写readObject并抛出无效对象异常以阻止默认序列化。此外,它覆盖clone()并抛出clone not supported异常。就反射而言,Enum的构造函数是受保护的。因此,如果您使用上面的代码,它将抛出NoSuchMethodFound。
即使使用getDeclaredConstructor()而不是getConstructor,也应该得到相同的异常。我认为它是通过java中的SecurityManager限制的。