Blog (2)
Komentarze (1.9k)
Recenzje (0)
@budda86Java tricks: hakowanie enuma

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.

Wybrane dla Ciebie
Komentarze (5)