在本新闻通讯中,该新闻通讯最初发表在Java专家的新闻通讯第161期中,我们研究了如何使用sun.reflect包中的反射类在Sun JDK中创建枚举实例。 显然,这仅适用于Sun的JDK。 如果需要在另一个JVM上执行此操作,则您可以自己完成。
这一切都始于爱丁堡的肯·多布森(Ken Dobson)的一封电子邮件,该电子邮件向我指出了sun.reflect.ConstructorAccessor
的方向,他声称可以将其用于构造枚举实例。 我以前的方法(通讯#141)在Java 6中不起作用。
我很好奇为什么Ken要构造枚举。 这是他想使用它的方式:
public enum HumanState {
HAPPY, SAD
}
public class Human {
public void sing(HumanState state) {
switch (state) {
case HAPPY:
singHappySong();
break;
case SAD:
singDirge();
break;
default:
new IllegalStateException("Invalid State: " + state);
}
}
private void singHappySong() {
System.out.println("When you're happy and you know it ...");
}
private void singDirge() {
System.out.println("Don't cry for me Argentina, ...");
}
}
上面的代码需要进行单元测试。 你发现错误了吗? 如果没有,请使用细梳再次遍历代码以尝试找到它。 当我第一次看到这个时,我也没有发现错误。
当我们产生这样的错误时,我们应该做的第一件事就是进行显示该错误的单元测试。 但是,在这种情况下,我们无法使default
情况发生,因为HumanState仅具有HAPPY和SAD枚举。
Ken的发现使我们可以使用sun.reflect包中的ConstructorAccessor类来创建枚举的实例。 它涉及到以下内容:
Constructor cstr = clazz.getDeclaredConstructor(
String.class, int.class
);
ReflectionFactory reflection =
ReflectionFactory.getReflectionFactory();
Enum e =
reflection.newConstructorAccessor(cstr).newInstance("BLA",3);
但是,如果仅执行此操作,则最终会出现ArrayIndexOutOfBoundsException,当我们看到Java编译器如何将switch语句转换为字节代码时,这才有意义。 以上面的Human类为例,反编译后的代码如下所示(感谢Pavel Kouznetsov的JAD ):
public class Human {
public void sing(HumanState state) {
static class _cls1 {
static final int $SwitchMap$HumanState[] =
new int[HumanState.values().length];
static {
try {
$SwitchMap$HumanState[HumanState.HAPPY.ordinal()] = 1;
} catch(NoSuchFieldError ex) { }
try {
$SwitchMap$HumanState[HumanState.SAD.ordinal()] = 2;
} catch(NoSuchFieldError ex) { }
}
}
switch(_cls1.$SwitchMap$HumanState[state.ordinal()]) {
case 1:
singHappySong();
break;
case 2:
singDirge();
break;
default:
new IllegalStateException("Invalid State: " + state);
break;
}
}
private void singHappySong() {
System.out.println("When you're happy and you know it ...");
}
private void singDirge() {
System.out.println("Don't cry for me Argentina, ...");
}
}
您可以立即看到为什么要得到ArrayIndexOutOfBoundsException,这要归功于内部类_cls1。
我第一次尝试解决此问题并没有得到一个不错的解决方案。 我试图在HumanState枚举中修改$ VALUES数组。 但是,我只是摆脱了Java的保护性代码。 您可以修改final字段 ,只要它们是非静态的即可。 对我来说,这种限制似乎是人为的,因此我着手寻找静态最终领域的圣杯。 再一次,它被藏在阳光反射的房间里。
设置“最终静态”字段
设置final static
字段需要几件事。 首先,我们需要使用法线反射获取Field对象。 如果将其传递给FieldAccessor,我们将退出安全代码,因为我们正在处理静态的final字段。 其次,我们将Field对象实例中的修饰符字段值更改为非最终值。 第三,将经过修改的字段传递给sun.reflect包中的FieldAccessor并使用它进行设置。
这是我的ReflectionHelper类,可用于通过反射设置final static
字段:
import sun.reflect.*;
import java.lang.reflect.*;
public class ReflectionHelper {
private static final String MODIFIERS_FIELD = "modifiers";
private static final ReflectionFactory reflection =
ReflectionFactory.getReflectionFactory();
public static void setStaticFinalField(
Field field, Object value)
throws NoSuchFieldException, IllegalAccessException {
// we mark the field to be public
field.setAccessible(true);
// next we change the modifier in the Field instance to
// not be final anymore, thus tricking reflection into
// letting us modify the static final field
Field modifiersField =
Field.class.getDeclaredField(MODIFIERS_FIELD);
modifiersField.setAccessible(true);
int modifiers = modifiersField.getInt(field);
// blank out the final bit in the modifiers int
modifiers &= ~Modifier.FINAL;
modifiersField.setInt(field, modifiers);
FieldAccessor fa = reflection.newFieldAccessor(
field, false
);
fa.set(null, value);
}
}
通过使用ReflectionHelper,我可以在枚举中设置$ VALUES数组以包含新的枚举。 这行得通,只是我必须在首次加载Human类之前执行此操作。 这会将竞争条件引入我们的测试用例中。 单独进行每个测试都可以,但是总的来说,它们可能会失败。 不好的情况!
重新连接枚举开关
下一个想法是重新连接实际的switch语句的$ SwitchMap $ HumanState字段。 在匿名内部类中找到该字段将相当容易。 您所需要的只是前缀$ SwitchMap $,后跟枚举类名称。 如果枚举在一个类中切换了几次,则内部类仅创建一次。
我昨天写的其他解决方案之一检查了我们的switch语句是否正在处理所有可能的情况。 将新类型引入系统后,这对于发现错误很有用。 我放弃了该特定解决方案,但是您应该能够基于稍后将向您展示的EnumBuster重新创建该解决方案。
纪念品设计模式
我最近重新编写了我的设计模式课程 (警告,该网站可能尚未建立最新的结构–请查询更多信息),以考虑Java的变化,丢弃一些过时的模式并介绍我以前排除的一些。 “新”模式之一是Memento,通常与撤消功能一起使用。 我认为这是一个很好的模式,可以用来在我们努力测试不可能的案例的努力中消除对枚举造成的损害。
出版专家通讯给我某些自由。 我不必解释我写的每一行。 因此,事不宜迟,这里是我的EnumBuster类,它使您可以创建枚举,将它们添加到现有的values []中,从数组中删除枚举,同时维护您指定的任何类的switch语句。
import sun.reflect.*;
import java.lang.reflect.*;
import java.util.*;
public class EnumBuster<E extends Enum<E>> {
private static final Class[] EMPTY_CLASS_ARRAY =
new Class[0];
private static final Object[] EMPTY_OBJECT_ARRAY =
new Object[0];
private static final String VALUES_FIELD = "$VALUES";
private static final String ORDINAL_FIELD = "ordinal";
private final ReflectionFactory reflection =
ReflectionFactory.getReflectionFactory();
private final Class<E> clazz;
private final Collection<Field> switchFields;
private final Deque<Memento> undoStack =
new LinkedList<Memento>();
/**
* Construct an EnumBuster for the given enum class and keep
* the switch statements of the classes specified in
* switchUsers in sync with the enum values.
*/
public EnumBuster(Class<E> clazz, Class... switchUsers) {
try {
this.clazz = clazz;
switchFields = findRelatedSwitchFields(switchUsers);
} catch (Exception e) {
throw new IllegalArgumentException(
"Could not create the class", e);
}
}
/**
* Make a new enum instance, without adding it to the values
* array and using the default ordinal of 0.
*/
public E make(String value) {
return make(value, 0,
EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);
}
/**
* Make a new enum instance with the given ordinal.
*/
public E make(String value, int ordinal) {
return make(value, ordinal,
EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);
}
/**
* Make a new enum instance with the given value, ordinal and
* additional parameters. The additionalTypes is used to match
* the constructor accurately.
*/
public E make(String value, int ordinal,
Class[] additionalTypes, Object[] additional) {
try {
undoStack.push(new Memento());
ConstructorAccessor ca = findConstructorAccessor(
additionalTypes, clazz);
return constructEnum(clazz, ca, value,
ordinal, additional);
} catch (Exception e) {
throw new IllegalArgumentException(
"Could not create enum", e);
}
}
/**
* This method adds the given enum into the array
* inside the enum class. If the enum already
* contains that particular value, then the value
* is overwritten with our enum. Otherwise it is
* added at the end of the array.
*
* In addition, if there is a constant field in the
* enum class pointing to an enum with our value,
* then we replace that with our enum instance.
*
* The ordinal is either set to the existing position
* or to the last value.
*
* Warning: This should probably never be called,
* since it can cause permanent changes to the enum
* values. Use only in extreme conditions.
*
* @param e the enum to add
*/
public void addByValue(E e) {
try {
undoStack.push(new Memento());
Field valuesField = findValuesField();
// we get the current Enum[]
E[] values = values();
for (int i = 0; i < values.length; i++) {
E value = values[i];
if (value.name().equals(e.name())) {
setOrdinal(e, value.ordinal());
values[i] = e;
replaceConstant(e);
return;
}
}
// we did not find it in the existing array, thus
// append it to the array
E[] newValues =
Arrays.copyOf(values, values.length + 1);
newValues[newValues.length - 1] = e;
ReflectionHelper.setStaticFinalField(
valuesField, newValues);
int ordinal = newValues.length - 1;
setOrdinal(e, ordinal);
addSwitchCase();
} catch (Exception ex) {
throw new IllegalArgumentException(
"Could not set the enum", ex);
}
}
/**
* We delete the enum from the values array and set the
* constant pointer to null.
*
* @param e the enum to delete from the type.
* @return true if the enum was found and deleted;
* false otherwise
*/
public boolean deleteByValue(E e) {
if (e == null) throw new NullPointerException();
try {
undoStack.push(new Memento());
// we get the current E[]
E[] values = values();
for (int i = 0; i < values.length; i++) {
E value = values[i];
if (value.name().equals(e.name())) {
E[] newValues =
Arrays.copyOf(values, values.length - 1);
System.arraycopy(values, i + 1, newValues, i,
values.length - i - 1);
for (int j = i; j < newValues.length; j++) {
setOrdinal(newValues[j], j);
}
Field valuesField = findValuesField();
ReflectionHelper.setStaticFinalField(
valuesField, newValues);
removeSwitchCase(i);
blankOutConstant(e);
return true;
}
}
} catch (Exception ex) {
throw new IllegalArgumentException(
"Could not set the enum", ex);
}
return false;
}
/**
* Undo the state right back to the beginning when the
* EnumBuster was created.
*/
public void restore() {
while (undo()) {
//
}
}
/**
* Undo the previous operation.
*/
public boolean undo() {
try {
Memento memento = undoStack.poll();
if (memento == null) return false;
memento.undo();
return true;
} catch (Exception e) {
throw new IllegalStateException("Could not undo", e);
}
}
private ConstructorAccessor findConstructorAccessor(
Class[] additionalParameterTypes,
Class<E> clazz) throws NoSuchMethodException {
Class[] parameterTypes =
new Class[additionalParameterTypes.length + 2];
parameterTypes[0] = String.class;
parameterTypes[1] = int.class;
System.arraycopy(
additionalParameterTypes, 0,
parameterTypes, 2,
additionalParameterTypes.length);
Constructor<E> cstr = clazz.getDeclaredConstructor(
parameterTypes
);
return reflection.newConstructorAccessor(cstr);
}
private E constructEnum(Class<E> clazz,
ConstructorAccessor ca,
String value, int ordinal,
Object[] additional)
throws Exception {
Object[] parms = new Object[additional.length + 2];
parms[0] = value;
parms[1] = ordinal;
System.arraycopy(
additional, 0, parms, 2, additional.length);
return clazz.cast(ca.newInstance(parms));
}
/**
* The only time we ever add a new enum is at the end.
* Thus all we need to do is expand the switch map arrays
* by one empty slot.
*/
private void addSwitchCase() {
try {
for (Field switchField : switchFields) {
int[] switches = (int[]) switchField.get(null);
switches = Arrays.copyOf(switches, switches.length + 1);
ReflectionHelper.setStaticFinalField(
switchField, switches
);
}
} catch (Exception e) {
throw new IllegalArgumentException(
"Could not fix switch", e);
}
}
private void replaceConstant(E e)
throws IllegalAccessException, NoSuchFieldException {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equals(e.name())) {
ReflectionHelper.setStaticFinalField(
field, e
);
}
}
}
private void blankOutConstant(E e)
throws IllegalAccessException, NoSuchFieldException {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.getName().equals(e.name())) {
ReflectionHelper.setStaticFinalField(
field, null
);
}
}
}
private void setOrdinal(E e, int ordinal)
throws NoSuchFieldException, IllegalAccessException {
Field ordinalField = Enum.class.getDeclaredField(
ORDINAL_FIELD);
ordinalField.setAccessible(true);
ordinalField.set(e, ordinal);
}
/**
* Method to find the values field, set it to be accessible,
* and return it.
*
* @return the values array field for the enum.
* @throws NoSuchFieldException if the field could not be found
*/
private Field findValuesField()
throws NoSuchFieldException {
// first we find the static final array that holds
// the values in the enum class
Field valuesField = clazz.getDeclaredField(
VALUES_FIELD);
// we mark it to be public
valuesField.setAccessible(true);
return valuesField;
}
private Collection<Field> findRelatedSwitchFields(
Class[] switchUsers) {
Collection<Field> result = new ArrayList<Field>();
try {
for (Class switchUser : switchUsers) {
Class[] clazzes = switchUser.getDeclaredClasses();
for (Class suspect : clazzes) {
Field[] fields = suspect.getDeclaredFields();
for (Field field : fields) {
if (field.getName().startsWith("$SwitchMap$" +
clazz.getSimpleName())) {
field.setAccessible(true);
result.add(field);
}
}
}
}
} catch (Exception e) {
throw new IllegalArgumentException(
"Could not fix switch", e);
}
return result;
}
private void removeSwitchCase(int ordinal) {
try {
for (Field switchField : switchFields) {
int[] switches = (int[]) switchField.get(null);
int[] newSwitches = Arrays.copyOf(
switches, switches.length - 1);
System.arraycopy(switches, ordinal + 1, newSwitches,
ordinal, switches.length - ordinal - 1);
ReflectionHelper.setStaticFinalField(
switchField, newSwitches
);
}
} catch (Exception e) {
throw new IllegalArgumentException(
"Could not fix switch", e);
}
}
@SuppressWarnings("unchecked")
private E[] values()
throws NoSuchFieldException, IllegalAccessException {
Field valuesField = findValuesField();
return (E[]) valuesField.get(null);
}
private class Memento {
private final E[] values;
private final Map<Field, int[]> savedSwitchFieldValues =
new HashMap<Field, int[]>();
private Memento() throws IllegalAccessException {
try {
values = values().clone();
for (Field switchField : switchFields) {
int[] switchArray = (int[]) switchField.get(null);
savedSwitchFieldValues.put(switchField,
switchArray.clone());
}
} catch (Exception e) {
throw new IllegalArgumentException(
"Could not create the class", e);
}
}
private void undo() throws
NoSuchFieldException, IllegalAccessException {
Field valuesField = findValuesField();
ReflectionHelper.setStaticFinalField(valuesField, values);
for (int i = 0; i < values.length; i++) {
setOrdinal(values[i], i);
}
// reset all of the constants defined inside the enum
Map<String, E> valuesMap =
new HashMap<String, E>();
for (E e : values) {
valuesMap.put(e.name(), e);
}
Field[] constantEnumFields = clazz.getDeclaredFields();
for (Field constantEnumField : constantEnumFields) {
E en = valuesMap.get(constantEnumField.getName());
if (en != null) {
ReflectionHelper.setStaticFinalField(
constantEnumField, en
);
}
}
for (Map.Entry<Field, int[]> entry :
savedSwitchFieldValues.entrySet()) {
Field field = entry.getKey();
int[] mappings = entry.getValue();
ReflectionHelper.setStaticFinalField(field, mappings);
}
}
}
}
该类很长,可能仍然存在一些错误。 我是从旧金山到纽约的途中写的。 这是我们可以使用它来测试人类课程的方法:
import junit.framework.TestCase;
public class HumanTest extends TestCase {
public void testSingingAddingEnum() {
EnumBuster<HumanState> buster =
new EnumBuster<HumanState>(HumanState.class,
Human.class);
try {
Human heinz = new Human();
heinz.sing(HumanState.HAPPY);
heinz.sing(HumanState.SAD);
HumanState MELLOW = buster.make("MELLOW");
buster.addByValue(MELLOW);
System.out.println(Arrays.toString(HumanState.values()));
try {
heinz.sing(MELLOW);
fail("Should have caused an IllegalStateException");
}
catch (IllegalStateException success) { }
}
finally {
System.out.println("Restoring HumanState");
buster.restore();
System.out.println(Arrays.toString(HumanState.values()));
}
}
}
现在,此单元测试在前面显示的Human.java文件中显示了错误。 我们忘记添加throw
关键字!
When you're happy and you know it ...
Don't cry for me Argentina, ...
[HAPPY, SAD, MELLOW]
Restoring HumanState
[HAPPY, SAD]
AssertionFailedError: Should have caused an IllegalStateException
at HumanTest.testSingingAddingEnum(HumanTest.java:23)
EnumBuster类可以做的更多。 我们可以使用它来删除不需要的枚举。 如果我们指定switch语句是哪个类,则将同时维护这些类。 另外,我们可以还原到初始状态。 很多功能!
我注销之前的最后一个测试用例,我们将测试类添加到switch类中以进行维护。
import junit.framework.TestCase;
public class EnumSwitchTest extends TestCase {
public void testSingingDeletingEnum() {
EnumBuster<HumanState> buster =
new EnumBuster<HumanState>(HumanState.class,
EnumSwitchTest.class);
try {
for (HumanState state : HumanState.values()) {
switch (state) {
case HAPPY:
case SAD:
break;
default:
fail("Unknown state");
}
}
buster.deleteByValue(HumanState.HAPPY);
for (HumanState state : HumanState.values()) {
switch (state) {
case SAD:
break;
case HAPPY:
default:
fail("Unknown state");
}
}
buster.undo();
buster.deleteByValue(HumanState.SAD);
for (HumanState state : HumanState.values()) {
switch (state) {
case HAPPY:
break;
case SAD:
default:
fail("Unknown state");
}
}
buster.deleteByValue(HumanState.HAPPY);
for (HumanState state : HumanState.values()) {
switch (state) {
case HAPPY:
case SAD:
default:
fail("Unknown state");
}
}
} finally {
buster.restore();
}
}
}
EnumBuster甚至保留常量,因此,如果从values()中删除一个枚举,它将清空最终的静态字段。 如果重新添加,它将设置为新值。
肯·多布森(Ken Dobson)的想法以一种我不知道有可能的方式进行反思,真是令人愉悦。 (任何Sun工程师都读过这篇文章,请不要在Java的未来版本中插入这些漏洞!)
亲切的问候
亨氏
JavaSpecialists在您公司内提供所有课程。 更多信息 …
请务必阅读我们有关Java并发性的新课程。 请与我联系以获取更多信息。
关于Heinz M.Kabutz博士
自2000年以来,我一直在为Java专家社区写作。这很有趣。 当您与可能会喜欢的人分享这本书时,这会更加有趣。 如果他们前往www.javaspecialists.eu并将自己添加到列表中,则他们可以每个月获得新鲜的东西。
中继:这篇文章是Java Advent Calendar的一部分,并根据Creative Commons 3.0 Attribution许可获得许可。 如果您喜欢它,请通过共享,发推,FB,G +等来传播信息! 想为博客写文章吗? 我们正在寻找能够填补所有24个职位的贡献者,并希望能为您贡献力量! 联系Attila Balazs贡献力量!
参考资料:来自Java日历日历博客的JCG合作伙伴 Attila-Mihaly Balazs的“枚举枚举和修改”最终静态”字段 。
翻译自: https://www.javacodegeeks.com/2012/12/of-hacking-enums-and-modifying-final-static-fields.html