Java動態修改Enum實例


眾所周知,enum類型實例數量是固定的,甚至還被用來設計單例。但有時候仍然存在需要動態增加Enum實例的場景,這也並非一定是設計失敗,也可能是增加靈活性的實際需求,比如一些web框架,再比如HanLPFork HanLP 給HanLP點贊 關注HanLP中的動態用戶自定義詞性。然而最大的障礙是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的話,你大概會得到一個友好的提示:

屏幕快照 2016-03-17 上午11.50.35.png

不過你可能會說,這個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

http://www.javaspecialists.eu/archive/Issue161.html


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com