В этом бюллетене, первоначально опубликованном в выпуске 161 бюллетеня специалистов по Java, мы рассмотрим, как можно создавать экземпляры enum в Sun JDK, используя классы отражения из пакета sun.reflect. Это, очевидно, будет работать только для Sun JDK. Если вам нужно сделать это на другой JVM, вы сами по себе.
 Все это началось с электронного письма от Кена Добсона из Эдинбурга, которое указывало мне на направление sun.reflect.ConstructorAccessor , которое, как он утверждал, могло быть использовано для создания экземпляров enum.  Мой предыдущий подход (бюллетень № 141) не работал в Java 6. 
Мне было любопытно, почему Кен хотел создать перечисления. Вот как он хотел это использовать:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | publicenumHumanState {  HAPPY, SAD}publicclassHuman {  publicvoidsing(HumanState state) {    switch(state) {      caseHAPPY:        singHappySong();        break;      caseSAD:        singDirge();        break;      default:         newIllegalStateException("Invalid State: "+ state);    }  }  privatevoidsingHappySong() {    System.out.println("When you're happy and you know it ...");  }  privatevoidsingDirge() {    System.out.println("Don't cry for me Argentina, ...");  }} | 
Приведенный выше код нуждается в модульном тестировании. Вы заметили ошибку? Если вы этого не сделали, снова попробуйте найти код с помощью прекрасного гребня. Когда я впервые увидел это, я тоже не заметил ошибку.
  Когда мы делаем ошибки, как это, первое, что мы должны сделать, это создать модульный тест, который это показывает.  Однако в этом случае мы не можем вызвать случай по default , поскольку HumanState имеет только перечисления HAPPY и SAD. 
Открытие Кена позволило нам создать экземпляр перечисления с помощью класса ConstructorAccessor из пакета sun.reflect. Это будет включать что-то вроде:
| 1 2 3 4 5 6 7 | Constructor cstr = clazz.getDeclaredConstructor(  String.class, int.class);ReflectionFactory reflection =  ReflectionFactory.getReflectionFactory();Enum e =  reflection.newConstructorAccessor(cstr).newInstance("BLA",3); | 
Однако, если мы просто сделаем это, мы получим исключение ArrayIndexOutOfBoundsException, которое имеет смысл, когда мы увидим, как компилятор Java преобразует оператор switch в байтовый код. Взяв вышеупомянутый класс Human, вот как выглядит декомпилированный код (благодаря JAD Павла Кузнецова ):
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | publicclassHuman {  publicvoidsing(HumanState state) {    staticclass_cls1 {      staticfinalint$SwitchMap$HumanState[] =        newint[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()]) {      case1:        singHappySong();        break;      case2:        singDirge();        break;      default:        newIllegalStateException("Invalid State: "+ state);        break;    }  }  privatevoidsingHappySong() {    System.out.println("When you're happy and you know it ...");  }  privatevoidsingDirge() {    System.out.println("Don't cry for me Argentina, ...");  }} | 
Вы можете сразу увидеть, почему мы получили ArrayIndexOutOfBoundsException, благодаря внутреннему классу _cls1.
Моя первая попытка исправить эту проблему не привела к достойному решению. Я попытался изменить массив $ VALUES внутри перечисления HumanState. Однако я только что отскочил от защитного кода Java. Вы можете изменить конечные поля , если они не являются статичными. Это ограничение показалось мне искусственным, поэтому я отправился на поиски святого Грааля статических полей. Опять же, он был спрятан в камере солнца.
Установка окончательных статических полей
  Несколько вещей необходимо для того, чтобы установить final static поле.  Прежде всего, нам нужно получить объект Field, используя нормальное отражение.  Если мы передадим это в FieldAccessor, мы просто отскочит код безопасности, так как мы имеем дело со статическим конечным полем.  Во-вторых, мы изменяем значение поля модификаторов внутри экземпляра объекта Field, чтобы оно не было окончательным.  В-третьих, мы передаем поле doctored в FieldAccessor в пакете sun.reflect и используем его для его установки. 
  Вот мой класс ReflectionHelper, который мы можем использовать для установки final static полей с помощью отражения: 
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | importsun.reflect.*;importjava.lang.reflect.*;publicclassReflectionHelper {  privatestaticfinalString MODIFIERS_FIELD = "modifiers";  privatestaticfinalReflectionFactory reflection =      ReflectionFactory.getReflectionFactory();  publicstaticvoidsetStaticFinalField(      Field field, Object value)      throwsNoSuchFieldException, 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);    intmodifiers = 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 был загружен впервые. Это привело бы к гоночным условиям в наших тестах. Сами по себе каждый тест будет работать, но все вместе они могут провалиться. Не хороший сценарий!
Rewiring Enum Switches
Следующая идея состояла в том, чтобы переписать поле $ SwitchMap $ HumanState фактического оператора switch. Было бы довольно легко найти это поле внутри анонимного внутреннего класса. Все, что вам нужно, это префикс $ SwitchMap $, за которым следует имя класса enum. Если перечисление переключается несколько раз в одном классе, то внутренний класс создается только один раз.
Одно из других решений, которые я написал вчера, проверило, касается ли наш оператор switch всех возможных случаев. Это было бы полезно при обнаружении ошибок, когда в систему вводится новый тип. Я отказался от этого конкретного решения, но вы сможете легко восстановить его на основе EnumBuster, который я покажу вам позже.
Шаблон дизайна Memento
Недавно я переписал свой курс по шаблонам проектирования (предупреждаю, что на веб-сайте еще может не быть обновленной структуры — пожалуйста, запросите дополнительную информацию), чтобы учесть изменения в Java, отбросить некоторые устаревшие шаблоны и представить некоторые, которые я исключил ранее. Одним из «новых» паттернов был Memento, часто используемый с отменой функциональности. Я подумал, что было бы неплохо использовать шаблон для устранения ущерба, нанесенного enum в наших больших усилиях по проверке нашего невозможного случая.
Публикация бюллетеня специалистов дает мне определенные свободы. Мне не нужно объяснять каждую строчку, которую я пишу. Итак, без лишних слов, вот мой класс EnumBuster, который позволяет вам создавать перечисления, добавлять их к существующим значениям [], удалять перечисления из массива, в то же время поддерживая оператор switch любого указанного вами класса.
| 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | importsun.reflect.*;importjava.lang.reflect.*;importjava.util.*;publicclassEnumBuster<E extendsEnum<E>> {  privatestaticfinalClass[] EMPTY_CLASS_ARRAY =      newClass[0];  privatestaticfinalObject[] EMPTY_OBJECT_ARRAY =      newObject[0];  privatestaticfinalString VALUES_FIELD = "$VALUES";  privatestaticfinalString ORDINAL_FIELD = "ordinal";  privatefinalReflectionFactory reflection =      ReflectionFactory.getReflectionFactory();  privatefinalClass<E> clazz;  privatefinalCollection<Field> switchFields;  privatefinalDeque<Memento> undoStack =      newLinkedList<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.   */  publicEnumBuster(Class<E> clazz, Class... switchUsers) {    try{      this.clazz = clazz;      switchFields = findRelatedSwitchFields(switchUsers);    } catch(Exception e) {      thrownewIllegalArgumentException(          "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.   */  publicE make(String value) {    returnmake(value, 0,        EMPTY_CLASS_ARRAY, EMPTY_OBJECT_ARRAY);  }  /**   * Make a new enum instance with the given ordinal.   */  publicE make(String value, intordinal) {    returnmake(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.   */  publicE make(String value, intordinal,                Class[] additionalTypes, Object[] additional) {    try{      undoStack.push(newMemento());      ConstructorAccessor ca = findConstructorAccessor(          additionalTypes, clazz);      returnconstructEnum(clazz, ca, value,          ordinal, additional);    } catch(Exception e) {      thrownewIllegalArgumentException(          "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   */  publicvoidaddByValue(E e) {    try{      undoStack.push(newMemento());      Field valuesField = findValuesField();      // we get the current Enum[]      E[] values = values();      for(inti = 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);      intordinal = newValues.length - 1;      setOrdinal(e, ordinal);      addSwitchCase();    } catch(Exception ex) {      thrownewIllegalArgumentException(          "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   */  publicbooleandeleteByValue(E e) {    if(e == null) thrownewNullPointerException();    try{      undoStack.push(newMemento());      // we get the current E[]      E[] values = values();      for(inti = 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(intj = i; j < newValues.length; j++) {            setOrdinal(newValues[j], j);          }          Field valuesField = findValuesField();          ReflectionHelper.setStaticFinalField(              valuesField, newValues);          removeSwitchCase(i);          blankOutConstant(e);          returntrue;        }      }    } catch(Exception ex) {      thrownewIllegalArgumentException(          "Could not set the enum", ex);    }    returnfalse;  }  /**   * Undo the state right back to the beginning when the   * EnumBuster was created.   */  publicvoidrestore() {    while(undo()) {      //    }  }  /**   * Undo the previous operation.   */  publicbooleanundo() {    try{      Memento memento = undoStack.poll();      if(memento == null) returnfalse;      memento.undo();      returntrue;    } catch(Exception e) {      thrownewIllegalStateException("Could not undo", e);    }  }  privateConstructorAccessor findConstructorAccessor(      Class[] additionalParameterTypes,      Class<E> clazz) throwsNoSuchMethodException {    Class[] parameterTypes =        newClass[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    );    returnreflection.newConstructorAccessor(cstr);  }  privateE constructEnum(Class<E> clazz,                          ConstructorAccessor ca,                          String value, intordinal,                          Object[] additional)      throwsException {    Object[] parms = newObject[additional.length + 2];    parms[0] = value;    parms[1] = ordinal;    System.arraycopy(        additional, 0, parms, 2, additional.length);    returnclazz.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.   */  privatevoidaddSwitchCase() {    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) {      thrownewIllegalArgumentException(          "Could not fix switch", e);    }  }  privatevoidreplaceConstant(E e)      throwsIllegalAccessException, NoSuchFieldException {    Field[] fields = clazz.getDeclaredFields();    for(Field field : fields) {      if(field.getName().equals(e.name())) {        ReflectionHelper.setStaticFinalField(            field, e        );      }    }  }  privatevoidblankOutConstant(E e)      throwsIllegalAccessException, NoSuchFieldException {    Field[] fields = clazz.getDeclaredFields();    for(Field field : fields) {      if(field.getName().equals(e.name())) {        ReflectionHelper.setStaticFinalField(            field, null        );      }    }  }  privatevoidsetOrdinal(E e, intordinal)      throwsNoSuchFieldException, 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   */  privateField findValuesField()      throwsNoSuchFieldException {    // 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);    returnvaluesField;  }  privateCollection<Field> findRelatedSwitchFields(      Class[] switchUsers) {    Collection<Field> result = newArrayList<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) {      thrownewIllegalArgumentException(          "Could not fix switch", e);    }    returnresult;  }  privatevoidremoveSwitchCase(intordinal) {    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) {      thrownewIllegalArgumentException(          "Could not fix switch", e);    }  }  @SuppressWarnings("unchecked")  privateE[] values()      throwsNoSuchFieldException, IllegalAccessException {    Field valuesField = findValuesField();    return(E[]) valuesField.get(null);  }  privateclassMemento {    privatefinalE[] values;    privatefinalMap<Field, int[]> savedSwitchFieldValues =        newHashMap<Field, int[]>();    privateMemento() throwsIllegalAccessException {      try{        values = values().clone();        for(Field switchField : switchFields) {          int[] switchArray = (int[]) switchField.get(null);          savedSwitchFieldValues.put(switchField,              switchArray.clone());        }      } catch(Exception e) {        thrownewIllegalArgumentException(            "Could not create the class", e);      }    }    privatevoidundo() throws        NoSuchFieldException, IllegalAccessException {      Field valuesField = findValuesField();      ReflectionHelper.setStaticFinalField(valuesField, values);      for(inti = 0; i < values.length; i++) {        setOrdinal(values[i], i);      }      // reset all of the constants defined inside the enum      Map<String, E> valuesMap =          newHashMap<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);      }    }  }} | 
Класс довольно длинный и, вероятно, все еще содержит некоторые ошибки. Я написал это по пути из Сан-Франциско в Нью-Йорк. Вот как мы могли бы использовать его для тестирования нашего класса Human:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | importjunit.framework.TestCase;publicclassHumanTest extendsTestCase {  publicvoidtestSingingAddingEnum() {    EnumBuster<HumanState> buster =        newEnumBuster<HumanState>(HumanState.class,            Human.class);    try{      Human heinz = newHuman();      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 ! 
| 1 2 3 4 5 6 7 8 | When you're happy and you know it ...Don't cry forme Argentina, ...[HAPPY, SAD, MELLOW]Restoring HumanState[HAPPY, SAD]AssertionFailedError: Should have caused an IllegalStateException  at HumanTest.testSingingAddingEnum(HumanTest.java:23) | 
Класс EnumBuster может сделать больше, чем это. Мы можем использовать его для удаления перечислений, которые нам не нужны. Если мы укажем, к каким классам относятся операторы switch, они будут поддерживаться одновременно. Кроме того, мы можем отменить прямо в исходное состояние. Много функциональности!
Еще один последний тестовый пример перед тем, как я выйду из системы, и мы добавим тестовый класс в классы коммутатора для поддержки.
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | importjunit.framework.TestCase;publicclassEnumSwitchTest extendsTestCase {  publicvoidtestSingingDeletingEnum() {    EnumBuster<HumanState> buster =        newEnumBuster<HumanState>(HumanState.class,            EnumSwitchTest.class);    try{      for(HumanState state : HumanState.values()) {        switch(state) {          caseHAPPY:          caseSAD:            break;          default:            fail("Unknown state");        }      }      buster.deleteByValue(HumanState.HAPPY);      for(HumanState state : HumanState.values()) {        switch(state) {          caseSAD:            break;          caseHAPPY:          default:            fail("Unknown state");        }      }      buster.undo();      buster.deleteByValue(HumanState.SAD);      for(HumanState state : HumanState.values()) {        switch(state) {          caseHAPPY:            break;          caseSAD:          default:            fail("Unknown state");        }      }      buster.deleteByValue(HumanState.HAPPY);      for(HumanState state : HumanState.values()) {        switch(state) {          caseHAPPY:          caseSAD:          default:            fail("Unknown state");        }      }    } finally{      buster.restore();    }  }} | 
EnumBuster даже поддерживает константы, поэтому, если вы удалите enum из значений (), он очистит окончательное статическое поле. Если вы добавите его обратно, он установит новое значение.
Было очень интересно использовать идеи Кена Добсона, чтобы поиграть с отражением так, как я не знал, было возможно. (Любые инженеры Sun, читающие это, не закрывайте эти дыры в будущих версиях Java!)
С уважением
Heinz
  JavaSpecialists предлагает все курсы в вашей компании.  Дополнительная информация … 
  Обязательно ознакомьтесь с нашим новым курсом по параллелизму Java .  Пожалуйста, свяжитесь со мной для получения дополнительной информации. 
О докторе Хайнце М. Кабуце
Я пишу для сообщества специалистов по Java с 2000 года. Это было весело. Еще веселее, когда ты делишься этим сочинением с кем-то, кому, как ты думаешь, оно понравится. И они могут обновлять его каждый месяц, если они направляются на www.javaspecialists.eu и добавляют себя в список.
Мета: этот пост является частью Java Advent Calendar и лицензирован под лицензией Creative Commons 3.0 Attribution . Если вам это нравится, пожалуйста, распространите информацию, поделившись, чирикать, FB, G + и так далее! Хотите написать для блога? Мы ищем участников для заполнения всех 24 слотов и хотели бы получить ваш вклад! Свяжитесь с Attila Balazs, чтобы внести свой вклад!
Ссылка: о взломе перечислений и изменении «окончательных статических» полей от нашего партнера по JCG Аттилы-Михали Балаза в блоге Java Advent Calendar .