Wyjątki w Kotlinie
Wyjątek to zazwyczaj niepożądane zdarzenie, które przerywa działanie programu. Może wystąpić, gdy wykonasz niedozwoloną operację. Wyjątki zawierają informacje, które pomagają deweloperom dowiedzieć się, co doprowadziło do problemu.
Przyjrzyjmy się przykładowi. Gdy podzielisz liczbę całkowitą przez 0, zostanie rzucony wyjątek typu ArithmeticException
. Każdy wyjątek może zawierać wiadomość, która powinna wyjaśnić, co poszło nie tak. W tym przypadku wiadomością będzie "/ by zero". Każdy wyjątek zawiera również stacktrace, czyli listę wywołań metod, określających gdzie znajdowała się aplikacja, gdy rzucony został wyjątek. W tym przypadku obejmuje informacje, że wyjątek został rzucony z funkcji calculate
, która została wywołana z funkcji printCalculated
, która została wywołana z funkcji main
. Wyjątek przerywa wykonanie programu, więc polecenia po nim nie zostaną wykonane. W poniższym przykładzie zauważ, że "After" nigdy nie zostaje wypisane.
Jako kolejny przykład możemy przekształcić stringa na liczbę całkowitą za pomocą metody toInt
. Ta operacja działa tylko wtedy, gdy string jest liczbą. Gdy tak nie jest, rzucony zostanie wyjątek NumberFormatException
z informacją, jaki string został użyty.
Rzucanie wyjątków
Możemy rzucać wyjątki samodzielnie, używając słowa kluczowego throw
i wartości, która może być użyta jako wyjątek, takiej jak wspomniane ArithmeticException
czy NumberFormatException
.
Wyjątki informują, że wystąpiła sytuacja, na którą funkcja nie jest przygotowana, lub której nie akceptuje. Nie jest to koniecznie oznaka błędu; to raczej zdarzenie powiadamiające, które może być obsłużone w innym miejscu, skonfigurowanym, aby rzucony wyjątek przechwycić.
Definiowanie wyjątków
Możemy definiować własne wyjątki. Są to klasy lub deklaracje obiektów, które rozszerzają klasę Throwable
. Każda instancja wyjątku może być rzucona za pomocą throw
.
Przechwytywanie wyjątków
Wyjątki rzucamy przy użyciu słówka throw
, a łapiemy je przy pomocy bloku catch
w konstrukcji try-catch. Aby złapać wyjątek, potrzebna jest cała struktura try-catch, która zawiera blok try i blok catch. Wyjątek rzucony w funkcji natychmiast kończy jej wykonanie, a proces powtarza się w funkcji, która ją wywołała i w której rzucony został wyjątek. To się zmienia, gdy wyjątek zostanie rzucony wewnątrz bloku try, ponieważ wtedy sprawdzane są jego bloki catch. Każdy blok catch może określić, jakiego rodzaju wyjątki przechwytuje. Pierwszy blok catch, który akceptuje rzucony wyjątek, przechwytuje go, a następnie wykonuje swoje ciało. Jeśli wyjątek zostanie przechwycony, wykonanie programu będzie kontynuowane po bloku try.
Zobaczmy w akcji try-catch z większą liczbą bloków catch. Pamiętaj, że zawsze wybierany jest pierwszy blok, który akceptuje rzucony wyjątek. Blok catch akceptuje wyjątek, jeśli jest on podtypem typu określonego w bloku catch. Wszystkie wyjątki muszą rozszerzać Throwable
, więc przechwytywanie tego typu oznacza przechwytywanie wszystkich możliwych wyjątków. Z tego powodu stosując więcej niż jeden block catch, ważne jest zachowanie odpowiedniej kolejności przechwytywania: od najbardziej do najmniej szczegółowego wyjątku.
Blok try-catch użyty jako wyrażenie
Struktura try-catch może być używana jako wyrażenie. Zwraca ona wynik bloku try, jeśli nie wystąpił żaden wyjątek. Jeśli wystąpi wyjątek i zostanie przechwycony, wyrażenie try-catch zwraca wynik bloku catch.
Wyrażenie try-catch może być używane do zapewnienia alternatywnej wartości w sytuacji, w której występuje problem. W poniższym kodzie próbujemy odczytać zawartość pliku, który nie istnieje, więc funkcja readText
rzuci wyjątek FileNotFoundException
. My ten wyjątek przechwytujemy, po czym zwracamy pusty string. W ten sposób możemy kontynuować działanie programu.
Praktycznym przykładem może być odczytanie ciągu znaków zawierającego obiekt w formacie JSON. Używamy biblioteki Gson, której metoda fromJson
rzuca wyjątek JsonSyntaxException
, gdy string nie zawiera prawidłowego obiektu JSON. W takich przypadkach wolelibyśmy funkcję zwracającą wartość null
; możemy to zaimplementować, używając try-catch jako wyrażenia.
Blok finally
W strukturze try można również użyć bloku finally. Jego zadaniem jest określenie, co powinno być zawsze wywołane, nawet jeśli wystąpi wyjątek. Ten blok nie przechwytuje żadnych wyjątków; jest używany, aby zagwarantować, że pewne operacje zostaną wykonane, niezależnie od wyjątków.
Spójrz na poniższy kod. Wyjątek jest rzucony wewnątrz someFunction
, czym kończy wykonanie funkcji i pomija resztę bloku try. Ponieważ nie mamy bloku catch, ten wyjątek nie zostanie złapany, a więc zakończy wykonanie funkcji main
. Jednak istnieje także blok finally, którego ciało jest wywoływane, nawet jeśli wystąpi wyjątek.
Blok finally jest również wywoływany wtedy, gdy blok try zakończy się bez wyjątku.
Blok finally używamy do wykonywania operacji, które zawsze powinny być wykonywane, niezależnie od tego, czy wystąpi wyjątek, czy nie. Zwykle obejmuje to zamykanie połączeń lub czyszczenie zasobów.
Ważne wyjątki
W Kotlinie zdefiniowano kilka rodzajów wyjątków, które stosujemy w określonych sytuacjach. Najważniejsze z nich to:
IllegalArgumentException
- używamy tego wyjątku, gdy argument ma nieprawidłową wartość. Na przykład, gdy oczekujemy, że wartość argumentu będzie większa niż 0, ale tak nie jest.IllegalStateException
- używamy tego wyjątku, gdy stan naszego systemu jest niepoprawny. Oznacza to, że wartości właściwości nie są akceptowane przez wywołanie funkcji.
W Kotlinie używamy funkcji require
i check
, aby odpowiednio zgłosić wyjątki IllegalArgumentException
i IllegalStateException
, gdy określone przez te funkcje warunki nie są spełnionee1.
W bibliotece standardowej Kotlin znajduje się również funkcja error
, która rzuca wyjątek IllegalArgumentException
z wiadomością określoną jako argument. Często używana jest jako ciało dla gałęzi w wyrażeniu warunkowym when, a także po prawej stronie operatora Elvisa lub w wyrażeniu if-else.
Hierarchia wyjątków
Najważniejsze podtypy Throwable
to Error
i Exception
. Reprezentują one dwa rodzaje wyjątków:
- Typ
Error
reprezentuje wyjątki, po których dalsze, poprawne działanie programu nie powinno być możliwe i które nie powinny być obsługiwane, a przynajmniej nie bez ponownego rzucenia ich w bloku catch. Dobrym przykładem jestOutOfMemoryError
, który jest rzucany, gdy naszej aplikacji skończy się pamięć. - Typ
Exception
reprezentuje wyjątki, które można złapać w bloku catch. Ta grupa obejmujeIllegalArgumentException
,IllegalStateException
,ArithmeticException
orazNumberFormatException
.
W większości przypadków do definiowania własnych wyjątków powinniśmy używać nadklasy Exception
; gdy przechwytujemy wyjątki, powinniśmy zgłaszać tylko podtypy klasy Exception
.
W Kotlinie nie jesteśmy zmuszeni do łapania żadnych rodzajów wyjątków, w przeciwieństwie do niektórych innych języków.
Podsumowanie
Z tego rozdziału dowiedzieliśmy się o wyjątkach, które są ważną częścią programowania w Kotlinie. Nauczyliśmy się, jak rzucać, łapać i definiować wyjątki. Dowiedzieliśmy się również o bloku finally oraz hierarchii wyjątków.
Kontynuując tematykę specjalnych rodzajów klas, porozmawiajmy o enumach, które są używane do reprezentowania zestawu możliwych wartości.
Ten temat jest lepiej opisany w książce Effective Kotlin, Temat 5: Określ swoje oczekiwania względem argumentów i stanu.