В этом бюллетене, первоначально опубликованном в выпуске 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
|
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, ..." ); } } |
Приведенный выше код нуждается в модульном тестировании. Вы заметили ошибку? Если вы этого не сделали, снова попробуйте найти код с помощью прекрасного гребня. Когда я впервые увидел это, я тоже не заметил ошибку.
Когда мы делаем ошибки, как это, первое, что мы должны сделать, это создать модульный тест, который это показывает. Однако в этом случае мы не можем вызвать случай по 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
|
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.
Моя первая попытка исправить эту проблему не привела к достойному решению. Я попытался изменить массив $ 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
|
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 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 ); int modifiers = 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
|
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 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 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() 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); } } } } |
Класс довольно длинный и, вероятно, все еще содержит некоторые ошибки. Я написал это по пути из Сан-Франциско в Нью-Йорк. Вот как мы могли бы использовать его для тестирования нашего класса 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
|
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
!
1
2
3
4
5
6
7
8
|
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 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
|
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 даже поддерживает константы, поэтому, если вы удалите 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 .