Java tricks: hakowanie enuma
16.06.2012 20:36
Każdy programista mający do czynienia z Javą 1.5 lub nowszą prawdopodobnie spotkał się z wyliczeniowym typem danych, czyli enumem. Enum to zamknięta lista wartości (stałych), ustalona już w momencie kompilacji, dzięki czemu w czasie działania programu zbiór tych wartości jest dokładnie znany i nie może zostać zmieniony. Z poprzednim zdaniem zgodzi się zdecydowana większość developerów, którzy nie czytali tego wpisu :) Jeśli chcecie wiedzieć, jak w runtime tworzyć nowe instancje enumów, czytajcie dalej.
Czym jest enum
Tak naprawdę enum to (nie)zwykła klasa, w której zdefiniowane są statyczne pola przechowujące jej instancje - stałe enuma. A więc deklaracja:
enum Language { POLISH, ENGLISH; }
może być (w uproszczeniu) rozumiana jako:
class Language extends java.lang.Enum<Language> { public static final Language POLISH = new Language("POLISH", 0); public static final Language ENGLISH = new Language("ENGLISH", 1); private Language(String name, int ordinal) { super(name, ordinal); } }
Wynika z tego, że stałe enuma to instancje klasy dziedziczącej po java.lang.Enum, czyli zwykłe obiekty. Klasa Enum jest jednak traktowana przez Javę w specjalny sposób i nie możemy bezpośrednio zadeklarować jej subklasy (tak jak powyżej) - powoduje to błąd kompilacji z komunikatem:
The type <NazwaTypu> may not subclass Enum explicitly
Dlaczego nie można utworzyć nowej instancji enuma
W specyfikacji Javy (Java Language Specification) czytamy, że w czasie działania programu nie mogą istnieć inne instancje danego enuma niż te, które umieściliśmy w jego deklaracji. Ograniczenie to jest realizowane przez cztery mechanizmy:
[list] [item]Nie można utworzyć nowej instancji enuma za pomocą słowa kluczowego new. Powoduje to błąd kompilacji z komunikatem:
Cannot instantiate the type <NazwaTypu>
[/item][item] Nie można wywołać konstruktora klasy Enum, ani żadnej jej podklasy. Co prawda refleksja pozwala nam na wywoływanie metod i konstruktorów normalnie niedostępnych (z modyfikatorami private, protected, package private), ale z enumem ta sztuczka nie przejdzie - dostaniemy wyjątek:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
[/item][item] Każde wywołane metody clone() na rzecz instancji enuma powoduje zgłoszenie wyjątku typu CloneNotSupportedException [/item][item] Istnieje specjalny mechanizm gwarantujący, że podczas deserializacji obiektów nie zostaną utworzone duplikaty istniejących instancji enuma. [/item][/list]
Ograniczenia te wyglądają na dość silne, ale...
Dlaczego jednak można to zrobić
Istnieje pewien magiczny pakiet, nazywający się, sun.misc, który wchodzi w skład standardowej dystrybucji Javy. Obecna w nim klasa sun.misc.Unsafe pozwala na różne ciekawe operacje, m.in. na bezpośrednie alokowanie i zwalnianie pamięci (niech żyje malloc). Udostępnia również metodę allocateInstance(Class), która tworzy nową instancję dowolnej klasy z pominięciem konstruktora. Tak jak pisałem wcześniej, niewidoczny konstruktor może być wywołany za pomocą refleksji, ale:
- Refleksja pozwala nam na wywołanie dowolnego istniejącego konstruktora klasy - z naciskiem na istniejącego. Metoda Unsafe.allocateInstance nie troszczy się o takie drobiazgi i bezpośrednio tworzy nową instancję nie wywołując żadnego konstruktora.
- Refleksja odmówi utworzenia instancji enuma, czego nie zrobi allocateInstance :)
Jak to można zrobić
Klasa Unsafe posiada statyczną metodę getUnsafe(), ale jej wywołanie w naszym kodzie spowoduje zgłoszenie wyjątku typu SecurityException. Możemy jednak za pomocą refleksji wyciągnąć obiekt typu Unsafe z prywatnego statycznego pola klasy Unsafe:
Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null);
(pomysł zaczerpnięty stąd )
I gotowe :) teraz możemy zrobić tak (zakładając, że mamy enuma Language - patrz początek wpisu):
Language newLang = (Language) unsafe.allocateInstance(Language.class);
Utworzona w ten sposób instancja enuma nie będzie zwracana przez metodę Language.values(), nie będzie mogła zostać pobrana przez Language.valueOf(String), a jej pola name i ordinal będą miały wartości odpowiednio null i 0. Dobra wiadomość jest taka, że wszystkie te problemy można rozwiącać za pomocą refleksji. Jeśli ktoś jest ciekawy jak - mogę napisać o tym (i udostępnić kod) w następnym wpisie.
Epilog
Możliwości metody allocateInstance mają też ograniczenia: [list] [item]Nie da się stworzyć instancji klas abstrakcyjnych i interfejsów (co jest jak najbardziej zrozumiałe).[/item][item] Nie da się stworzyć instancji tablicy, np. wywołanie:
unsafe.allocateInstance(String[].class);
Spowoduje zgłoszenie wyjątku java.lang.InstantiationException. Choć teoretycznie nie powinno być przeciwskazań do utworzenia tablicy, zwłaszcza, że można to zrobić za pomocą metody Array.newInstance(Class, int). [/item][item] Nie da się stworzyć instancji klasy reprezentującej typ prosty lub void - nie ma wyjątku, za to wywala się JVM :) U mnie:
# A fatal error has been detected by the Java Runtime Environment: # SIGSEGV (0xb) at pc=0xb6ed71ce, pid=7665, tid=3064728432
[/item][/list]
Są też inne sposoby na robienie rzeczy normalnie niedozwolonych w Javie. Istnieją biblioteki bezpośrednio manipulujące bajtkodem, również w runtime, takie jak Javassist. Ich potęga jest prawie nieograniczona ;) To jednak jest już wyższa magia i wykracza poza tematykę mojego wpisu.