Java Language
Pułapki Java - użycie wyjątku
Szukaj…
Wprowadzenie
Kilka błędów językowych w języku Java może spowodować, że program wygeneruje niepoprawne wyniki pomimo prawidłowej kompilacji. Głównym celem tego tematu jest lista typowych pułapek związanych z obsługą wyjątków oraz zaproponowanie właściwego sposobu uniknięcia takich pułapek.
Pitfall - ignorowanie lub zgniatanie wyjątków
Ten przykład dotyczy celowego ignorowania lub „zgniatania” wyjątków. Mówiąc ściślej, chodzi o to, jak złapać wyjątek i postępować z nim w sposób, który go ignoruje. Zanim jednak opiszemy, jak to zrobić, należy najpierw zauważyć, że wyjątki dotyczące zgniatania nie są na ogół właściwym sposobem radzenia sobie z nimi.
Wyjątki są zwykle zgłaszane (przez coś), aby powiadomić inne części programu o wystąpieniu jakiegoś znaczącego (tj. „Wyjątkowego”) zdarzenia. Ogólnie (choć nie zawsze) wyjątek oznacza, że coś poszło nie tak. Jeśli kodujesz swój program, aby zgnieść wyjątek, istnieje spora szansa, że problem pojawi się ponownie w innej formie. Co gorsza, zgniatając wyjątek, wyrzucasz informacje z obiektu wyjątku i powiązanego z nim śladu stosu. Prawdopodobnie utrudni to ustalenie pierwotnego źródła problemu.
W praktyce zgniatanie wyjątków często występuje, gdy używasz funkcji automatycznej korekty IDE w celu „naprawienia” błędu kompilacji spowodowanego nieobsługiwanym wyjątkiem. Na przykład możesz zobaczyć taki kod:
try {
inputStream = new FileInputStream("someFile");
} catch (IOException e) {
/* add exception handling code here */
}
Oczywiście programista zaakceptował sugestię IDE, aby błąd kompilacji zniknął, ale sugestia była nieodpowiednia. (Jeśli otwarcie pliku nie powiodło się, program najprawdopodobniej coś z tym zrobi. Przy powyższej „korekcie” program może później zawieść; np. Z NullPointerException
ponieważ inputStream
jest teraz null
).
Powiedziawszy to, oto przykład celowego zgniecenia wyjątku. (Dla celów dyskusji załóżmy, że stwierdziliśmy, że przerwanie podczas pokazywania selfie jest nieszkodliwe.) Komentarz mówi czytelnikowi, że celowo usunęliśmy wyjątek i dlaczego to zrobiliśmy.
try {
selfie.show();
} catch (InterruptedException e) {
// It doesn't matter if showing the selfie is interrupted.
}
Innym konwencjonalnym sposobem podkreślenia, że celowo zgniatamy wyjątek, nie mówiąc, dlaczego jest to wskazane za pomocą nazwy zmiennej wyjątku, na przykład:
try {
selfie.show();
} catch (InterruptedException ignored) { }
Niektóre IDE (jak IntelliJ IDEA) nie wyświetlą ostrzeżenia o pustym bloku catch, jeśli nazwa zmiennej jest ustawiona na ignored
.
Pitfall - Catching Throwable, Exception, Error lub RuntimeException
Częstym wzorcem myślenia niedoświadczonych programistów Java jest to, że wyjątki są „problemem” lub „obciążeniem”, a najlepszym sposobem na poradzenie sobie z tym jest złapanie ich wszystkich 1 tak szybko, jak to możliwe. Prowadzi to do takiego kodu:
....
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (Exception ex) {
System.out.println("Could not open file " + fileName);
}
Powyższy kod ma znaczną wadę. catch
rzeczywiście złapie więcej wyjątków, niż się spodziewa programista. Załóżmy, że wartość fileName
ma null
powodu błędu w innym miejscu aplikacji. Spowoduje to, że konstruktor FileInputStream
NullPointerException
. Program przechwytuje to i zgłasza użytkownikowi:
Could not open file null
co jest nieprzydatne i zagmatwane. Co gorsza, załóżmy, że to kod „przetworzył dane wejściowe” zgłosił nieoczekiwany wyjątek (zaznaczony lub niezaznaczony!). Teraz użytkownik otrzyma mylący komunikat dotyczący problemu, który nie wystąpił podczas otwierania pliku i może w ogóle nie być związany z operacjami we / wy.
Przyczyną problemu jest to, że programista zakodował moduł obsługi Exception
. To prawie zawsze błąd:
- Przechwytywanie
Exception
przechwytuje wszystkie zaznaczone wyjątki, a także większość niezaznaczonych wyjątków. - Catching
RuntimeException
przechwytuje większość niezaznaczonych wyjątków. -
Error
połowu wyłapie niezaznaczone wyjątki, które sygnalizują wewnętrzne błędy JVM. Błędy te zasadniczo nie są możliwe do odzyskania i nie należy ich wychwytywać. - Catching
Throwable
złapie wszystkie możliwe wyjątki.
Problem z przechwytywaniem zbyt szerokiego zestawu wyjątków polega na tym, że moduł obsługi zazwyczaj nie może odpowiednio obsłużyć wszystkich z nich. W przypadku Exception
i tak dalej programiści mają trudności z przewidywaniem, co może zostać złapane; czyli czego się spodziewać.
Ogólnie rzecz biorąc, poprawnym rozwiązaniem jest uporanie się z wyjątkami, które zostały zgłoszone. Na przykład możesz je złapać i obsłużyć je na miejscu:
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (FileNotFoundException ex) {
System.out.println("Could not open file " + fileName);
}
lub możesz zadeklarować je jako thrown
metodą załączającą.
Istnieje bardzo niewiele sytuacji, w których właściwe jest uchwycenie Exception
. Jedynym, który często się pojawia, jest coś takiego:
public static void main(String[] args) {
try {
// do stuff
} catch (Exception ex) {
System.err.println("Unfortunately an error has occurred. " +
"Please report this to X Y Z");
// Write stacktrace to a log file.
System.exit(1);
}
}
Tutaj naprawdę chcemy poradzić sobie ze wszystkimi wyjątkami, więc wychwytywanie Exception
(a nawet Throwable
) jest prawidłowe.
1 - Znany również jako obsługa wyjątków Pokemon .
Pitfall - rzucanie Throwable, Exception, Error lub RuntimeException
RuntimeException
wyjątków Throwable
, Exception
, Error
i RuntimeException
jest złe, rzucanie nimi jest jeszcze gorsze.
Podstawowym problemem jest to, że gdy aplikacja musi obsługiwać wyjątki, obecność wyjątków najwyższego poziomu utrudnia rozróżnienie między różnymi warunkami błędu. Na przykład
try {
InputStream is = new FileInputStream(someFile); // could throw IOException
...
if (somethingBad) {
throw new Exception(); // WRONG
}
} catch (IOException ex) {
System.err.println("cannot open ...");
} catch (Exception ex) {
System.err.println("something bad happened"); // WRONG
}
Problem polega na tym, że ponieważ Exception
instancję Exception
, jesteśmy zmuszeni ją złapać. Jednak, jak opisano w innym przykładzie, wyłapanie Exception
jest złe. W tej sytuacji, staje się trudne do rozróżnienia między „oczekiwanej” przypadku Exception
, który zostanie rzucony jeśli somethingBad
jest true
, a niespodziewanym przypadku, gdy rzeczywiście złapać niesprawdzony wyjątek takich jak NullPointerException
.
Jeśli można propagować wyjątek najwyższego poziomu, napotkamy inne problemy:
- Musimy teraz pamiętać o różnych przyczynach, dla których rzuciliśmy najwyższy poziom, i dyskryminować je / zajmować się nimi.
- W przypadku
Exception
iThrowable
musimy również dodać te wyjątki do klauzuli metodthrows
, jeśli chcemy, aby wyjątek się rozpowszechniał. Jest to problematyczne, jak opisano poniżej.
Krótko mówiąc, nie wyrzucaj tych wyjątków. Rzuć bardziej szczegółowy wyjątek, który bardziej szczegółowo opisuje „wyjątkowe wydarzenie”, które miało miejsce. Jeśli potrzebujesz, zdefiniuj i użyj niestandardowej klasy wyjątku.
Zgłaszanie „Throwable” lub „Exception” w „rzutach” metody jest problematyczne.
Kuszące jest zastąpienie długiej listy zgłaszanych wyjątków w klauzuli metody throws
Exception
lub nawet „Throwable”. To jest zły pomysł:
- Zmusza dzwoniącego do obsługi (lub propagowania)
Exception
. - Nie możemy już polegać na kompilatorze, który informuje nas o określonych sprawdzonych wyjątkach, które należy obsłużyć.
-
Exception
obsługaException
jest trudna. Trudno jest ustalić, jakie rzeczywiste wyjątki mogą zostać złapane, a jeśli nie wiesz, co można złapać, trudno jest ustalić, która strategia odzyskiwania jest odpowiednia. - Obsługa
Throwable
jest jeszcze trudniejsza, ponieważ teraz musisz również poradzić sobie z potencjalnymi awariami, których nigdy nie należy odzyskać.
Ta rada oznacza, że należy unikać pewnych innych wzorców. Na przykład:
try {
doSomething();
} catch (Exception ex) {
report(ex);
throw ex;
}
Powyższe próby rejestrowania wszystkich wyjątków podczas ich przechodzenia bez ostatecznego ich obsługi. Niestety, przed Java 7, throw ex;
Instrukcja spowodowała, że kompilator pomyślał, że można zgłosić dowolny Exception
. To może zmusić cię do zadeklarowania otaczającej metody jako throws Exception
. Począwszy od Java 7, kompilator wie, że zestaw wyjątków, które mogą być (ponownie rzucone), jest mniejszy.
Pitfall - Catching InterruptedException
Jak już wspomniano w innych pułapkach, wychwytywanie wszystkich wyjątków za pomocą
try {
// Some code
} catch (Exception) {
// Some error handling
}
Pochodzi z wieloma różnymi problemami. Ale jednym z poważnych problemów jest to, że może prowadzić do impasu, ponieważ psuje system przerwań podczas pisania aplikacji wielowątkowych.
Jeśli zaczynasz wątek, zwykle musisz być w stanie zatrzymać go nagle z różnych powodów.
Thread t = new Thread(new Runnable() {
public void run() {
while (true) {
//Do something indefinetely
}
}
}
t.start();
//Do something else
// The thread should be canceld if it is still active.
// A Better way to solve this is with a shared variable that is tested
// regularily by the thread for a clean exit, but for this example we try to
// forcibly interrupt this thread.
if (t.isAlive()) {
t.interrupt();
t.join();
}
//Continue with program
t.interrupt()
spowoduje zgłoszenie t.interrupt()
InterruptedException w tym wątku, niż ma to na celu zamknięcie wątku. Ale co jeśli Wątek musi oczyścić niektóre zasoby, zanim całkowicie się zatrzyma? W tym celu może przechwycić wyjątek InterruptedException i wykonać pewne czyszczenie.
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
//Do something indefinetely
}
} catch (InterruptedException ex) {
//Do some quick cleanup
// In this case a simple return would do.
// But if you are not 100% sure that the thread ends after
// catching the InterruptedException you will need to raise another
// one for the layers surrounding this code.
Thread.currentThread().interrupt();
}
}
}
Ale jeśli w kodzie znajduje się wyrażenie catch-all, InterruptedException również go przechwyci i przerwa nie będzie kontynuowana. Co w tym przypadku może doprowadzić do impasu, ponieważ wątek nadrzędny czeka w nieskończoność na zatrzymanie się tej metody przez t.join()
.
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
try {
//Do something indefinetely
}
catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (InterruptedException ex) {
// Dead code as the interrupt exception was already caught in
// the inner try-catch
Thread.currentThread().interrupt();
}
}
}
Lepiej więc wychwytywać wyjątki osobno, ale jeśli nalegasz na użycie polecenia catch-all, przynajmniej najpierw złap InterruptedException osobno.
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
try {
//Do something indefinetely
} catch (InterruptedException ex) {
throw ex; //Send it up in the chain
} catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (InterruptedException ex) {
// Some quick cleanup code
Thread.currentThread().interrupt();
}
}
}
Pitfall - stosowanie wyjątków dla normalnej kontroli przepływu
Istnieje pewna mantra, którą niektórzy eksperci Javy nie będą recytować:
„Wyjątki należy stosować tylko w wyjątkowych przypadkach”.
(Na przykład: http://programmers.stackexchange.com/questions/184654 )
Istotą tego jest to, że złym pomysłem (w Javie) jest stosowanie wyjątków i obsługi wyjątków w celu wdrożenia normalnej kontroli przepływu. Na przykład porównaj te dwa sposoby radzenia sobie z parametrem, który może mieć wartość NULL.
public String truncateWordOrNull(String word, int maxLength) {
if (word == null) {
return "";
} else {
return word.substring(0, Math.min(word.length(), maxLength));
}
}
public String truncateWordOrNull(String word, int maxLength) {
try {
return word.substring(0, Math.min(word.length(), maxLength));
} catch (NullPointerException ex) {
return "";
}
}
W tym przykładzie (z założenia) traktujemy przypadek, w którym word
ma null
tak jakby było pustym słowem. Dwie wersje zajmują się null
albo przy użyciu konwencjonalnej metody ... jeśli lub spróbuj ... złapać . Jak powinniśmy zdecydować, która wersja jest lepsza?
Pierwszym kryterium jest czytelność. Podczas gdy czytelność jest trudna do obiektywnego oszacowania, większość programistów zgodzi się, że podstawowe znaczenie pierwszej wersji jest łatwiejsze do rozpoznania. Rzeczywiście, aby naprawdę zrozumieć drugą formę, trzeba zrozumieć, że NullPointerException
nie mogą być wyrzucane przez Math.min
lub String.substring
metod.
Drugim kryterium jest wydajność. W wersjach Java wcześniejszych niż Java 8 druga wersja jest znacznie (o rząd wielkości) wolniejsza niż pierwsza wersja. W szczególności konstrukcja obiektu wyjątkowego wymaga przechwytywania i rejestrowania ramek stosu, na wypadek, gdy wymagany jest ślad stosu.
Z drugiej strony istnieje wiele sytuacji, w których stosowanie wyjątków jest bardziej czytelne, wydajniejsze i (czasami) bardziej poprawne niż stosowanie kodu warunkowego do obsługi „wyjątkowych” zdarzeń. Rzeczywiście zdarzają się rzadkie sytuacje, w których konieczne jest wykorzystanie ich do „wyjątkowych” wydarzeń; tj. zdarzenia, które występują stosunkowo często. W tym drugim przypadku warto przyjrzeć się sposobom zmniejszenia kosztów ogólnych związanych z tworzeniem wyjątków.
Pitfall - Nadmierne lub nieodpowiednie ślady stosu
Jedną z bardziej irytujących rzeczy, które mogą zrobić programiści, jest rozproszenie wywołań printStackTrace()
całym kodzie.
Problem polega na tym, że printStackTrace()
zapisuje stacktrace na standardowym wyjściu.
W przypadku aplikacji przeznaczonej dla użytkowników końcowych, którzy nie są programistami Java, ślad stosu jest co najwyżej nieinformacyjny, aw najgorszym przypadku alarmujący.
W przypadku aplikacji po stronie serwera istnieje prawdopodobieństwo, że nikt nie spojrzy na standardowe wyjście.
Lepszym pomysłem jest, aby nie wywoływać bezpośrednio printStackTrace
lub, jeśli je wywołasz, zrób to w taki sposób, aby ślad stosu był zapisywany w pliku dziennika lub pliku błędu, a nie w konsoli użytkownika końcowego.
Jednym ze sposobów na to jest użycie struktury rejestrowania i przekazanie obiektu wyjątku jako parametru zdarzenia dziennika. Jednak nawet rejestrowanie wyjątku może być szkodliwe, jeśli zostanie wykonane w nieuczciwy sposób. Rozważ następujące:
public void method1() throws SomeException {
try {
method2();
// Do something
} catch (SomeException ex) {
Logger.getLogger().warn("Something bad in method1", ex);
throw ex;
}
}
public void method2() throws SomeException {
try {
// Do something else
} catch (SomeException ex) {
Logger.getLogger().warn("Something bad in method2", ex);
throw ex;
}
}
Jeśli wyjątek zostanie method2
w method2
, prawdopodobnie zobaczysz dwie kopie tego samego śledzenia stosu w pliku dziennika, odpowiadające tej samej awarii.
Krótko mówiąc, albo zaloguj wyjątek, albo przerzuć go ponownie (ewentualnie zapakowany z innym wyjątkiem). Nie rób obu.
Pitfall - Bezpośrednio podklasę „Throwable”
Throwable
ma dwie bezpośrednie podklasy, Exception
i Error
. Chociaż możliwe jest utworzenie nowej klasy, która bezpośrednio rozszerza Throwable
, jest to niewskazane, ponieważ wiele aplikacji zakłada, że istnieją tylko Exception
i Error
.
Co więcej, praktyczna podklasa Throwable
nie ma praktycznych korzyści, ponieważ wynikowa klasa jest w rzeczywistości sprawdzonym wyjątkiem. Zamiast tego Exception
podklasowania spowoduje takie samo zachowanie, ale wyraźniej wyrazi Twoją intencję.