枚举枚举和修改“最终静态”字段的方法
在本新聞通訊中,該新聞通訊最初發表在Java專家的新聞通訊第161期中,我們研究了如何使用sun.reflect包中的反射類在Sun JDK中創建枚舉實例。 顯然,這僅適用于Sun的JDK。 如果需要在另一個JVM上執行此操作,則您可以自己完成。
這一切都始于愛丁堡的肯·多布森(Ken Dobson)的一封電子郵件,該電子郵件向我指出了sun.reflect.ConstructorAccessor的方向,他聲稱可以將其用于構造枚舉實例。 我以前的方法(通訊#141)在Java 6中不起作用。
我很好奇為什么Ken要構造枚舉。 這是他想使用它的方式:
上面的代碼需要進行單元測試。 你發現錯誤了嗎? 如果沒有,請使用細梳再次遍歷代碼以嘗試找到它。 當我第一次看到這個時,我也沒有發現錯誤。
當我們產生這樣的錯誤時,我們應該做的第一件事就是進行顯示該錯誤的單元測試。 但是,在這種情況下,我們無法使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 publicfield.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 fieldField modifiersField =Field.class.getDeclaredField(MODIFIERS_FIELD);modifiersField.setAccessible(true);int modifiers = modifiersField.getInt(field);// blank out the final bit in the modifiers intmodifiers &= ~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 arrayE[] 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 classField valuesField = clazz.getDeclaredField(VALUES_FIELD);// we mark it to be publicvaluesField.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() throwsNoSuchFieldException, 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 enumMap<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 IllegalStateExceptionat 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
總結
以上是生活随笔為你收集整理的枚举枚举和修改“最终静态”字段的方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 曝小米13T Pro将于9月16日全球发
- 下一篇: 特斯拉美国将 FSD 辅助驾驶功能从 1