天天看點

java 枚舉 動态_Java動态修改Enum執行個體

java 枚舉 動态_Java動态修改Enum執行個體

衆所周知,enum類型執行個體數量是固定的,甚至還被用來設計單例。但有時候仍然存在需要動态增加Enum執行個體的場景,這也并非一定是設計失敗,也可能是增加靈活性的實際需求,比如一些web架構,再比如HanLP

java 枚舉 動态_Java動态修改Enum執行個體
java 枚舉 動态_Java動态修改Enum執行個體
java 枚舉 動态_Java動态修改Enum執行個體

中的動态使用者自定義詞性。然而最大的障礙是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的話,你大概會得到一個友好的提示:

java 枚舉 動态_Java動态修改Enum執行個體

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

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.*;

public class EnumBuster>

{

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 clazz;

private final Collection switchFields;

private final Deque undoStack =

new LinkedList();

public EnumBuster(Class clazz, Class... switchUsers)

{

try

{

this.clazz = clazz;

switchFields = findRelatedSwitchFields(switchUsers);

}

catch (Exception e)

{

throw new IllegalArgumentException(

"Could not create the class", e);

}

}

public E make(String value)

{

return make(value, 0,

EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);

}

public E make(String value, int ordinal)

{

return make(value, ordinal,

EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);

}

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

}

}

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 

{

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

}

}

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 

{

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 

{

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;

}

public void restore()

{

while (undo())

{

//

}

}

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 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 cstr = clazz.getDeclaredConstructor(

parameterTypes

);

return reflection.newConstructorAccessor(cstr);

}

private E constructEnum(Class 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));

}

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

}

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 findRelatedSwitchFields(

Class[] switchUsers)

{

Collection result = new LinkedList();

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 savedSwitchFieldValues =

new HashMap();

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 

{

setOrdinal(values[i], i);

}

// reset all of the constants defined inside the enum

Map valuesMap =

new HashMap();

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 entry :

savedSwitchFieldValues.entrySet())

{

Field field = entry.getKey();

int[] mappings = entry.getValue();

ReflectionHelper.setStaticFinalField(field, mappings);

}

}

}

}

調用方式

EnumBuster buster =

new EnumBuster(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