Java动态修改Enum实例
众所周知,enum
类型实例数量是固定的,甚至还被用来设计单例。但有时候仍然存在需要动态增加Enum实例的场景,这也并非一定是设计失败,也可能是增加灵活性的实际需求,比如一些web框架。然而最大的障碍是switch
语句生成的虚构类,本文参考Java Specialists
第161期,提供一份可用的解决方案与实例代码。
一段有问题的代码
比如我们有一个enum类型:
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, ...");
}
}
问题在哪里?如果你使用Intelij IDEA的话,你大概会得到一个友好的提示:
不过你可能会说,这个switch
分支“永远”不会被触发,就算这句有问题也无伤大雅,甚至这个default
分支根本没有存在的必要。
真的吗?
触发不可能的switch分支
Enum
类也是类,既然是类,就能通过反射来创建实例,我们创建一个试试。
Constructor cstr = HumanState.class.getDeclaredConstructor(
String.class, int.class
);
ReflectionFactory reflection =
ReflectionFactory.getReflectionFactory();
HumanState e =
(HumanState) reflection.newConstructorAccessor(cstr).newInstance(new Object[]{"ANGRY", 3});
System.out.printf("%s = %d\n", e.toString(), e.ordinal());
Human human = new Human();
human.sing(e);
运行结果
结果出乎意料:
ANGRY = 3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
at com.hankcs.Human.sing(Human.java:21)
at com.hankcs.FireArrayIndexException.main(FireArrayIndexException.java:36)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
本来指望发生IllegalStateException
,怎么出了一个ArrayIndexOutOfBoundsException
?
探索问题
虽然我们成功地创建了一个新的Enum
实例,但我们却数组越界了。stacktrace
指出问题发生在:
switch (state)
这一句,我们不妨看看这一句编译后是什么样子的。借助IDEA的反编译插件,我们可以看到编译后反编译回来的代码:
public class Human {
public Human() {
}
public void sing(HumanState state) {
class Human$1 {
static {
try {
$SwitchMap$com$hankcs$HumanState[HumanState.HAPPY.ordinal()] = 1;
} catch (NoSuchFieldError var2) {
;
}
try {
$SwitchMap$com$hankcs$HumanState[HumanState.SAD.ordinal()] = 2;
} catch (NoSuchFieldError var1) {
;
}
}
}
switch(Human$1.$SwitchMap$com$hankcs$HumanState[state.ordinal()]) {
case 1:
this.singHappySong();
break;
case 2:
this.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, ...");
}
}
原来在switch
分支前面创建了一个静态内部类(其实是synthetic
类),该内部类有一个静态final
数组,该数组“缓存”了编译时的所有Enum
对象的ordinal
。当我们通过反射新增Enum
对象后,该数组并没有得到更新,所以发生了数组下标越界的异常。
解决问题
修改final static域
/**
* 修改final static域的反射工具
* @author hankcs
*/
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
{
// 获得 public 权限
field.setAccessible(true);
// 将modifiers域设为非final,这样就可以修改了
Field modifiersField =
Field.class.getDeclaredField(MODIFIERS_FIELD);
modifiersField.setAccessible(true);
int modifiers = modifiersField.getInt(field);
// 去掉 final 标志位
modifiers &= ~Modifier.FINAL;
modifiersField.setInt(field, modifiers);
FieldAccessor fa = reflection.newFieldAccessor(
field, false
);
fa.set(null, value);
}
}
修改涉及Enum的switch分支
既然这个缓存数组是叫$SwitchMap$HumanState
,我们需要修改所有以$SwitchMap$+Enum
名称的域。
参考原作者写了一个实现类(我主要修改了虚构类的获取方法,以适应jdk8):
package com.hankcs;
import sun.reflect.*;
import java.lang.reflect.*;
import java.util.*;
/**
* 动态修改Enum的对象
* @param <E>
*/
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.
* <p/>
* 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.
* <p/>
* The ordinal is either set to the existing position
* or to the last value.
* <p/>
* 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 LinkedList<Field>();
try
{
for (Class switchUser : switchUsers)
{
String name = switchUser.getName();
int i = 0;
while (true)
{
try
{
Class suspect = Class.forName(String.format("%s$%d", name, ++i));
Field[] fields = suspect.getDeclaredFields();
for (Field field : fields)
{
String fieldName = field.getName();
if (fieldName.startsWith("$SwitchMap$") && fieldName.endsWith(clazz.getSimpleName()))
{
field.setAccessible(true);
result.add(field);
}
}
}
catch (ClassNotFoundException e)
{
break;
}
}
}
}
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);
}
}
}
}
调用方式
EnumBuster<HumanState> buster =
new EnumBuster<HumanState>(HumanState.class,
Human.class);
HumanState ANGRY = buster.make("ANGRY");
buster.addByValue(ANGRY);
System.out.println(Arrays.toString(HumanState.values()));
Human human = new Human();
human.sing(ANGRY);
输出
[HAPPY, SAD, ANGRY]
switch
分支完美了。
没有发生异常,其实这才是最大的异常,那个default
分支明明进去了,可就是没有抛异常。为什么?因为我们忘了加throw
啊,朋友。
Reference
版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/26/java-dynamically-modifying-enum-instances/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论