Parę dni temu pomagałem rozwiązać problem, który nagle pojawił się w trakcie działania aplikacji sieciowej działającej w środowisku Java EE. Ostatecznie okazało się, że źródłem problemu była konfiguracja serwera bazy danych. Jednym z tropów, jaki pojawił się jednak w czasie śledztwa było konwertowanie wartości niezgodnych typów, czego objawem było pojawianie się w dziennikach zdarzeń aplikacji sytuacji wyjątkowej Przyczyną takiego stanu rzeczy jest sprytna optymalizacja stosowana przez maszynę wirtualną Java oraz sposób, w jaki zapisuje ona w dziennikach zdarzeń oznaczenia typów prostych.ClassCastException
. I choć później dowiedzieliśmy się, że był to tylko objaw, a nie źródło problemu, rozpoznanie powodu błędu konwersji było trudniejsze, niż można się było spodziewać.
Aplikacja przestała pracować poprawnie nad ranem, choć ta sama wersja przez cały poprzedni dzień działała bez zarzutu. Zasypała przy tym swoje dzienniki zdarzeń raportami z występowania sytuacji wyjątkowej o następującej postaci:
java.lang.ClassCastException: null
Niestety, niewiele to mówiło. Programista Java spodziewa się po bibliotece standardowej raczej dość jednoznacznych komunikatów, opisujących przyczynę problemu, a nawet podpowiadających sposoby rozwiązania. Tutaj za całą informację musiało wystarczyć… null
oraz znajomość klasy sytuacji wyjątkowej.
Okazuje się jednak, że nie stoi za tym błąd, lecz jedna z technik optymalizacyjnych stosowanych przez maszynę wirtualną Java. Dopóki kod jest interpretowany, przy każdym wystąpieniu błędu konwersji obiekt sytuacji wyjątkowej ClassCastException
jest tworzony od nowa i zawiera pełnię informacji o przyczynie wystąpienia. Gdy jednak maszyna wirtualna stwierdzi, że dany fragment programu jest „gorącym punktem” (ang. hot spot) i przeprowadzi jego kompilację JIT, kolejne zgłoszenia tej sytuacji wyjątkowej będą wykorzystywały jeden wspólny, predefiniowany obiekt. Dzięki temu zmniejsza się koszt czasowy i pamięciowy zgłoszenia standardowej sytuacji wyjątkowej, akceptowalny w czasie, gdy kod aplikacji jest interpretowany, lecz nieproporcjonalnie długi, gdy nastąpi jego kompilacja do postaci maszynowej.
Ta sama optymalizacja dotyczy kilku innych standardowych sytuacji wyjątkowych, w tym między innymi sławnego (lub niesławnego…) NullPointerException
. Można ją wyłączyć dodając do opcji maszyny wirtualnej parametr -XX:-OmitStackTraceInFastThrow
.
Jeżeli jednak mamy możliwość ponownego uruchomienia aplikacji, łatwo jest doprowadzić do sytuacji, w której miejsce wystąpienia błędu nie będzie jeszcze stanowiło „gorącego punktu” i sytuacja wyjątkowa ClassCastException
zostanie zgłoszona w „zwykły” sposób. Komunikat, który pojawi się wtedy jako jej opis, w połączeniu ze śladem realizacji programu powinien rozwiać nasze wątpliwości co do przyczyny błędu. Chyba, że na ekranie zobaczymy coś takiego:
java.lang.ClassCastException: [B cannot be cast to oracle.sql.STRUCT
Część komunikatu po prawej stronie jest jasna. Czym jednak jest [B
? Otóż dla typów prostych Java nie stosuje pełnych nazw, lecz jednoliterowe skróty. Dodatkowo, w przypadku tablic, skrót jest poprzedzany nawiasami kwadratowymi, których liczba odpowiada rzędowi tablicy. Poniższa tabela prezentuje skróty odpowiadające poszczególnym typom prostym.
Skrót | Nazwa typu |
---|---|
Z | boolean |
B | byte |
S | short |
C | char |
I | int |
J | long |
F | float |
D | double |
Kod [B
to zatem po prostu tablica bajtów byte[]
.
Informacje, które przedstawiłem w tym artykule na pewno nie są przydatne w codziennej pracy programisty języka Java. Warto jednak mieć pojęcie, dlaczego komunikat powiązany z obiektem sytuacji wyjątkowej może być pusty, choć zazwyczaj widzieliśmy w nim przydatne informacje, lub skąd biorą się dziwne, pozornie niepoprawne nazwy typów danych.
Zaś tym, którzy zastanawiają się, co oznaczał odszukany po tylu trudach błąd konwersji byte[]
na oracle.sql.STRUCT
mogę podpowiedzieć, że winna była biblioteka third-party automatycznie generująca treść zapytań SQL, która przy błędnej konfiguracji uprawnień bazy danych powielała nazwy kolumn i oczekiwała danych geometrycznych w miejscu, w którym baza przysyłała identyfikator wiersza.