Dzielenie przez zero - obsługa wyjątków
Dzielenie przez 0 bez obsługi wyjątków
Najpierw pokażemy, co się stanie, gdy pojawi się błąd w aplikacji, która nie używa obsługi wyjątków. Rysunek 11.2 przedstawia kod, który prosi użytkownika o wpisanie dwóch liczb całkowitych i przekazuje je do metody quotient, która wylicza ich iloraz i zwraca wynik typu int. W przykładzie zobaczysz, że metoda, która odkryła problem i nie potrafi go samodzielnie rozwiązać, zgłasza
(rzuca) wyjątek.
2 // Dzielenie liczb całkowitych bez obsługi wyjątków
3 import java.util.Scanner;
4
5 public class DivideByZeroNoExceptionHandling {
6 // Przykład zgłoszenia wyjątku w momencie dzielenia przez 0
7 public static int quotient(int numerator, int denominator) {
8 return numerator / denominator; // Możliwe dzielenie przez 0
9 }
10
11 public static void main(String[] args) {
12 Scanner scanner = new Scanner(System.in);
13
14 System.out.print("Wpisz licznik (liczba całkowita): ");
15 int numerator = scanner.nextInt();
16 System.out.print("Wpisz mianownik (liczba całkowita): ");
17 int denominator = scanner.nextInt();
18
19 int result = quotient(numerator, denominator);
20 System.out.printf(
21 "%nWynik: %d / %d = %d%n", numerator, denominator, result);
22 }
23 }
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): 7
Wynik: 100 / 7 = 14
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
at DivideByZeroNoExceptionHandling.quotient(
DivideByZeroNoExceptionHandling.java:8)
at DivideByZeroNoExceptionHandling.main(
DivideByZeroNoExceptionHandling.java:19)
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): witaj
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Unknown Source)
at java.base/java.util.Scanner.next(Unknown Source)
at java.base/java.util.Scanner.nextInt(Unknown Source)
at java.base/java.util.Scanner.nextInt(Unknown Source)
at DivideByZeroNoExceptionHandling.main(
DivideByZeroNoExceptionHandling.java:17)
Rysunek 11.2. Dzielenie liczb całkowitych bez obsługi wyjątków
Zrzut stosu
Pierwsze spośród przykładowych wykonań z rysunku 11.2 przedstawia poprawne dzielenie. W drugim przykładzie użytkownik wpisał 0 jako mianownik. Z powodu niepoprawnych danych pojawiło się kilka wierszy z informacją o błędzie. Te informacje to zrzut stosu prowadzący do wyjątku, który zawiera nazwę wyjątku (java.lang.ArithmeticException) wraz z opisem powodu zgłoszenia wyjątku,
a także stos wywołań (zbiór ramek stosu) w momencie wystąpienia wyjątku. Zrzut stosu zawiera pełną ścieżkę wykonania prowadzącą metoda po metodzie do miejsca wystąpienia wyjątku. W ten sposób łatwiej znaleźć przyczynę błędu. Nawet jeśli nie wystąpił żaden wyjątek, możesz dokonać zrzutu metod na stosie, wywołując metodę Thread.dumpStack().
Zrzut stosu dla wyjątku ArithmeticException
Pierwszy wiersz wskazuje, że wystąpił wyjątek ArithmeticException. Tekst po nazwie wyjątku ("/ by zero") wskazuje, że wyjątek wystąpił z powodu próby dzielenia przez 0. Java nie dopuszcza do dzielenia przez 0 liczb całkowitych. W takiej sytuacji zgłasza wyjątek ArithmeticException. Ponieważ sam wyjątek może pojawić się również przy innych okazjach, dodatkowa informacja ("/ by
zero") pozwala poznać szczegóły.
Przejdźmy przez stos od końca. Widzimy, że wyjątek został wykryty w wierszu 19. metody main. Każdy wiersz stosu wywołań zawiera nazwę klasy i nazwę metody (np. DivideByZeroNoExceptionHandling.main), po których pojawia się nazwa pliku i numer wiersza (np. DivideByZeroNoExceptionHandling.java:19). Przechodząc wyżej, widzimy, że wyjątek wystąpił w wierszu 8. metody quotient. Najwyżej położone wywołanie to miejsce zgłoszenia — czyli źródło wyjątku. Źródłem zgłoszenia jest więc wiersz 8. metody quotient.
Uwaga na temat arytmetyki zmiennoprzecinkowej
Java dopuszcza dzielenie przez zero liczb zmiennoprzecinkowych. Takie działanie prowadzi do dodatniej lub ujemnej nieskończoności, co reprezentuje liczba zmiennoprzecinkowa wyświetlana jako "Infinity" lub "-Infinity". Jeśli podzielimy
0,0 przez 0,0, wynikiem będzie wartość NaN (nieliczba), wyświetlana jako tekst "NaN". Jeżeli chcesz sprawdzić, czy liczba zmiennoprzecinkowa jest równa NaN, użyj metody isNaN klasy Float (dla typu float) lub klasy Double (dla typu double). Obie klasy znajdują się w pakiecie java.lang.
Zrzut stosu dla wyjątku InputMismatchException
W trzecim wykonaniu kodu użytkownik wpisał "witaj" jako mianownik. Ponownie pojawił się zrzut stosu. Poinformował nas o wystąpieniu wyjątku InputMismatchException (pakiet java.util). Nasz przykład zakładał, że użytkownik wprowadzi poprawną wartość. Użytkownicy popełniają jednak błędy i czasem wpisują wartości niebędące liczbami całkowitymi. Wyjątek InputMismatchException
ma miejsce, gdy metoda nextInt klasy Scanner otrzyma tekst, który nie jest poprawną liczbą całkowitą. Ponownie zaczynamy czytać stos wywołań od końca. Widzimy, że wyjątek został wykryty w wierszu 17. metody main. Przechodząc wyżej, widzimy metodę nextInt. Zwróć uwagę, że zamiast nazwy pliku i numeru wiersza pojawia się tekst Unknown Source. Oznacza to, że tak zwane informacje debugowania zawierające nazwę pliku i numer wiersza dla tej klasy były dla JVM niedostępne — dotyczy to najczęściej klas z API Javy. Większość IDE ma dostęp do kodu źródłowego API Javy i wyświetli odpowiednie nazwy plików i numery
wierszy.
Zakończenie programu
Po wyświetleniu zrzutu stosu z rysunku 11.2 spowodowanego wyjątkiem program kończy działanie. Nie zawsze tak się jednak dzieje. Czasem program działa nadal pomimo wystąpienia wyjątku i wyświetlenia zrzutu stosu. W takiej sytuacji aplikacja często przestaje działać stabilnie. Najczęściej kontynuowanie programu wynika z dalszej obsługi interfejsu graficznego użytkownika. Na rysunku 11.2 oba rodzaje wyjątków dotyczyły metody main. W następnym przykładzie zobaczysz, jak obsłużyć oba wyjątki, aby program zakończył działanie w normalny sposób.
Obsługa wyjątków ArithmeticException i InputMismatchException
Aplikacja z rysunku 11.3 bazująca na rysunku 11.2 używa obsługi wyjątków do przetworzenia dowolnych wyjątków typu ArithmeticException i InputMismatchException. Aplikacja ponownie prosi o dwie liczby całkowite i przekazuje je do metody quotient, która oblicza iloraz i zwraca wynik. Ta wersja aplikacji używa obsługi wyjątków, więc jeśli użytkownik popełni błąd, program wychwyci i obsłuży wyjątek — w tym przypadku poprosi ponownie o wpisanie wartości.
1 // Rysunek 11.3. DivideByZeroWithExceptionHandling.java
2 // Obsługa wyjątków ArithmeticExceptions i InputMismatchExceptions
3 import java.util.InputMismatchException;
4 import java.util.Scanner;
5
6 public class DivideByZeroWithExceptionHandling
7 {
8 // Przykład zgłoszenia wyjątku w momencie dzielenia przez 0
9 public static int quotient(int numerator, int denominator)
10 throws ArithmeticException {
11 return numerator / denominator; // Możliwe dzielenie przez 0
12 }
13
14 public static void main(String[] args) {
15 Scanner scanner = new Scanner(System.in);
16 boolean continueLoop = true; // Określa, czy potrzebne są nadal dane wejściowe
17
18 do {
19 try { // Pobiera dwie liczby i wyświetla wynik
20 System.out.print("Wpisz licznik (liczba całkowita): ");
21 int numerator = scanner.nextInt();
22 System.out.print("Wpisz mianownik (liczba całkowita): ");
23 int denominator = scanner.nextInt();
24
25 int result = quotient(numerator, denominator);
26 System.out.printf("%nWynik: %d / %d = %d%n", numerator,
27 denominator, result);
28 continueLoop = false; // Udało się pobrać wartości i wyświetlić wynik; zakończ pętlę
29 }
30 catch (InputMismatchException inputMismatchException) {
31 System.err.printf("%nWyjątek: %s%n",
32 inputMismatchException);
33 scanner.nextLine(); // Pomiń dane, aby użytkownik mógł spróbować ponownie
34 System.out.printf(
35 "Musisz wpisać liczby całkowite. Spróbuj ponownie.%n%n");
36 }
37 catch (ArithmeticException arithmeticException) {
38 System.err.printf("%nWyjątek: %s%n", arithmeticException);
39 System.out.printf(
40 "Zero nie jest poprawnym mianownikiem. Spróbuj ponownie.%n%n");
41 }
42 } while (continueLoop);
43 }
44 }
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): 7
Wynik: 100 / 7 = 14
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): 0
Wyjątek: java.lang.ArithmeticException: / by zero
Zero nie jest poprawnym mianownikiem. Spróbuj ponownie.
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): 7
Wynik: 100 / 7 = 14
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): witaj
Wyjątek: java.util.InputMismatchException
Musisz wpisać liczby całkowite. Spróbuj ponownie.
Wpisz licznik (liczba całkowita): 100
Wpisz mianownik (liczba całkowita): 7
Wynik: 100 / 7 = 14
Rysunek 11.3. Obsługa wyjątków ArithmeticExceptions i InputMismatchExceptions
Pierwsze przykładowe wykonanie z rysunku 11.3 nie napotkało żadnych problemów. W drugim przykładzie użytkownik wpisał zero jako mianownik, co doprowadziło do wyjątku ArithmeticException. W trzecim przykładzie użytkownik wpisał "witaj" jako wartość mianownika, co spowodowało wyjątek InputMismatchException. W obu sytuacjach użytkownik został powiadomiony o błędzie
i poproszony o ponowne wpisanie wartości, czyli dwóch liczb całkowitych. W obu przykładach program działa poprawnie pomimo przeciwności.
Klasę InputMismatchException importujemy w wierszu 3. Klasy ArithmeticException nie musimy importować, ponieważ znajduje się w pakiecie java.lang. Wiersz 16. tworzy zmienną continueLoop typu boolean, która jest równa true, jeśli użytkownik nie wpisał poprawnych wartości. Wiersze od 18. do 42. proszą o ponowne wpisanie wartości, aż nie otrzymają poprawnych danych.
Zamykanie kodu w bloku try
Wiersze od 19. do 29. zawierają blok try. Znajduje się w nim kod, który może zgłosić wyjątek, i kod, który nie powinien się wykonać, jeśli pojawi się wyjątek (w momencie wystąpienia wyjątku pozostały kod z bloku try jest pomijany). Blok try składa się ze słowa kluczowego try i bloku kodu umieszczonego w nawiasach klamrowych. (Uwaga: Termin „blok try” odnosi się czasem jedynie
do bloku kodu za słowem kluczowym try i nie obejmuje tego słowa. Dla uproszczenie będziemy jednak nazywać blokiem try blok kodu wraz ze słowem kluczowym). Instrukcje odczytujące liczby całkowite wprowadzone z klawiatury (wiersze 21. i 23.) używają metody nextInt do odczytania wartości. Metoda nextInt zgłasza wyjątek InputMismatchException, jeśli odczytana wartość nie jest
liczbą całkowitą.
Dzielenie, które powoduje wyjątek typu ArithmeticException, nie ma miejsca w bloku try. Wywołanie metody quotient (wiersz 25.) przechodzi do kodu, który próbuje dzielić wartości (wiersz 11.); JVM zgłasza wyjątek ArithmeticException, gdy wykryje, że mianownikiem jest 0.
Obserwacja z poziomu inżynierii oprogramowania
Wyjątki mogą zostać zgłoszone przez kod znajdujący się bezpośrednio w bloku try, przez głęboko zagnieżdżone metody wywoływane z poziomu bloku try lub przez maszynę wirtualną Javy wykonującą kod bajtowy.
Wychwytywanie wyjątków
Po bloku try w przedstawionym przykładzie pojawiają się dwa bloki catch: jeden obsługuje wyjątek InputMismatchException (wiersze od 30. do 36.), a drugi wyjątek ArithmeticException (wiersze od 37. do 41.). Blok catch (nazywany też procedurą
obsługi wyjątków lub klauzulą catch) wychwytuje (czyli otrzymuje) i obsługuje wyjątek. Blok catch rozpoczyna się od słowa kluczowego catch i parametru podanego w nawiasach (to parametr wyjątku, o którym za chwilę), a następnie pojawia się blok kodu umieszczony w nawiasach klamrowych. Przynajmniej jeden blok catch lub blok finally (podrozdział 11.6) musi pojawić się tuż za blokiem try. Każdy blok catch wskazuje w nawiasach parametr wyjątku, który identyfikuje typ wyjątku obsługiwany przez proces. Gdy w bloku try wystąpi wyjątek, zostanie on obsłużony przez pierwszy blok catch, który pasuje do zgłoszonego obiektu wyjątku (czyli jest wskazanego typu lub jego bezpośrednią bądź pośrednią podklasą). Nazwa parametru wyjątku pozwala blokowi
catch pobrać obiekt wyjątku i w dowolny sposób go przetworzyć — na przykład wywołując metodę toString (wiersze 31. i 32. oraz 38.), która zwraca dodatkowe informacje na temat wyjątku. Zauważ, że użyliśmy standardowego obiektu obsługi błędów (System.err) do wyświetlenia informacji o błędzie. Domyślnie obiekt ten wyświetla przekazane mu wartości w konsoli w taki sam sposób jak obiekt System.out.
Wiersz 33. pierwszego bloku catch wywołuje metodę nextLine obiektu Scanner. Ponieważ wystąpił wyjątek InputMismatchException, wywołaniu metody nextInt nie udało się w całości odczytać danych od użytkownika, więc odczytujemy je metodą nextLine. Nie robimy nic z otrzymanymi danymi, bo wiemy, że są niepoprawne. Każdy blok catch wyświetla komunikat o błędzie i prośbę o ponowne wpisanie danych. Po zakończeniu każdego bloku catch użytkownik jest dzięki pętli proszony ponownie o dane. Przyjrzymy się wkrótce, jak ten przepływ sterowania działa w przypadku obsługi wyjątków.
Typowy błąd programistyczny
Jeśli umieścisz jakikolwiek kod między blokiem try i blokiem catch, kompilator zgłosi błąd składniowy.
Blok catch obsługujący wiele typów wyjątków
Dosyć często zdarza się, że po bloku try pojawia się kilka bloków catch obsługujących różne rodzaje wyjątków. Jeśli treści kilku bloków catch są identyczne, można użyć bloku dotyczącego wielu wyjątków, aby za pomocą jednego kodu obsłużyć wiele różnych sytuacji. Składnia tej wersji ma postać:
catch (Typ1 | Typ2 | Typ3 e)
Każdy typ wyjątku jest oddzielony od następnego znakiem pionowej kreski (|). Kod oznacza, że ta procedura obsługi może dotyczyć któregokolwiek z typów (lub ich podklas). W konstrukcji może pojawić się dowolna liczba typów dziedziczących po Throwable. W tej sytuacji parametr wyjątku jest wspólną klasą nadrzędną dla wszystkich wymienionych typów.
Niewychwycone wyjątki
Niewychwycony wyjątek to taki, dla którego brakuje pasującego bloku catch. Przykładami niewychwyconych wyjątków były drugi i trzeci wynik z rysunku 11.2. Przypomnijmy, że po zajściu wyjątku w tym przykładzie aplikacja zakończyła się wcześniej (tuż po wyświetleniu zrzutu stosu). Zakończenie programu nie zawsze ma miejsce. Java używa modelu „wielowątkowego” do wykonywania
programów — każdy wątek to niezależna aktywność. Jeden program może mieć wiele wątków. Jeśli program wykorzystuje tylko jeden wątek, niewychwycony wyjątek spowoduje jego przerwanie. Jeśli program ma wiele wątków, niewychwycony wyjątek spowoduje przerwanie tylko tego wątku, w którym wystąpił wyjątek. W takim programie pewne wątki korzystają z innych, więc jeżeli
jeden z nich nieoczekiwanie się zakończy, może to negatywnie wpłynąć na pozostałą część programu. W rozdziale 23. dokładniej opisujemy ten temat.
Model przerywania kodu w obsłudze wyjątków
Jeśli wyjątek nastąpi w bloku try (np. InputMismatchException zostanie zgłoszone w wyniku wykonania kodu z wiersza 23. na rysunku 11.3), blok ten od razu przerwie wykonywanie kodu i przekaże sterowanie do pierwszego bloku catch pasującego do zgłoszonego wyjątku. Na rysunku 11.3 pierwszy blok wychwytuje wyjątki InputMismatchException (wpisanie niepoprawnych danych), a drugi wychwytuje wyjątki ArithmeticException (próba dzielenia przez zero). Po obsłużeniu wyjątku sterowanie nie wraca do miejsca wywołania, ponieważ blok try stracił ważność (utracono jego zmienne lokalne). Kontynuacja programu odbywa
się od miejsca zakończenia ostatniego bloku catch. To tak zwany model przerywania kodu w obsłudze wyjątków. Niektóre języki używają modelu przywracania w obsłudze wyjątków, w którym to po obsłużeniu wyjątku sterowanie wraca do miejsca tuż po miejscu jego zgłoszenia.
Zauważ, że nazywamy parametry wyjątków (inputMismatchException i arithmeticException) na podstawie ich typów. Programiści Javy bardzo często stosują literę e jako nazwę parametru wyjątku.
Po zakończeniu bloku catch sterowanie programu powraca do pierwszej instrukcji tuż po ostatnim bloku catch (w tym przypadku do wiersza 42.). Warunek w instrukcji do...while jest prawdziwy (zmienna continueLoop zawiera początkową wartość równą true), więc sterowanie wraca na początek pętli i prosi ponownie o wprowadzenie danych. Pętla będzie działała dopóty, dopóki nie
zostaną wprowadzone poprawne dane. W takiej sytuacji sterowanie programu przejdzie do wiersza 28., który ustawi zmienną continueLoop na wartość false. Kończy to blok try. Jeśli w bloku wyjątek nie wystąpił, bloki catch są pomijane i sterowanie przechodzi do pierwszej instrukcji po blokach try (alternatywną wersję opiszemy w podrozdziale 11.6, dotyczącym bloku finally). Teraz warunek pętli do...while będzie równy false, więc dojdziemy do końca metody main.
Blok try i następujące po nim bloki catch lub (i) finally tworzą instrukcję try. Nie należy mylić bloku try z instrukcją try — ta druga zawiera nie tylko blok try, ale również wszystkie znajdujące się bezpośrednio za nim bloki catch i finally.
Podobnie jak w przypadku każdego innego bloku kodu, po zakończeniu bloku try wszystkie zadeklarowane w nim zmienne lokalne stają się niedostępne (wychodzą poza swój zasięg), a po zakończeniu bloku catch wszystkie zadeklarowane w nim zmienne lokalne stają się niedostępne i są niszczone (dotyczy to również parametru wyjątku). Pozostałe bloki catch instrukcji try są ignorowane,
a kod jest kontynuowany od pierwszego wiersza po sekwencji try...catch (np. bloku finally, jeśli istnieje).
Klauzula throws
W metodzie quotient (wiersze od 9. do 12. na rysunku 11.3) wiersz 10. nazywany jest klauzulą throws. Klauzula ta, która pojawia się po liście parametrów metody, ale przed treścią metody, zawiera listę rodzajów oddzielonych przecinkami wyjątków. Wyjątki te mogą być zgłaszane przez instrukcje metody lub kod wywoływany przez te instrukcje. Dodaliśmy w aplikacji klauzulę throws do
metody, aby wskazać, że może zgłosić wyjątek ArithmeticException. Kod wywołujący quotient jest więc „świadom”, że metoda może zgłosić taki wyjątek. Niektóre rodzaje wyjątków, w tym ArithmeticException, nie są wymagane do zgłaszania na liście throws. Dla tych, które są podane, metoda może zgłosić w wyjątku dowolny obiekt znajdujący się w relacji „jest” z klasą wskazaną w klauzuli throws.
Wskazówka zapobiegająca błędom
Przeczytaj dokumentację API przed użyciem metod API we własnym programie. Dokumentacja wskazuje wyjątki zgłaszane przez metody (o ile istnieją) i opisuje sytuacje, w których mogą się pojawić. Następnie przeczytaj dokumentację API związaną z klasą wyjątku. Zawiera ona często wyjaśnienie powodów zgłaszania takiego wyjątku. Na końcu dodaj obsługę tych wyjątków do kodu programu.
Gdy wykonuje się wiersz 11., a zmienna denominator zawiera wartość 0, JVM zgłosi wyjątek ArithmeticException. Obiekt wyjątku zostanie wychwycony przez blok catch z wierszy od 37. do 41., który wyświetli podstawowe informacje na temat wyjątku, niejawnie wywołując metodę toString i prosząc o ponowne wpisanie danych.
Jeśli zmienna denominator zawiera wartość różną od zera, metoda quotient dokona dzielenia i zwróci wynik do miejsca wywołania metody w bloku try (wiersz 25.) Wiersze 26. i 27. wyświetlają wynik obliczeń, a wiersz 28. ustawia continueLoop na false. W takiej sytuacji blok try kończy się bez błędów, więc program pomija bloki catch, nie spełnia warunku z wiersza 42. i w normalny
sposób kończy wykonywanie metody main.
Gdy metoda quotient zgłosi wyjątek ArithmeticException, metoda przerywa działanie i nie zwraca wartości, a lokalne zmienne metody tracą ważność (są niszczone). Gdyby metoda zawierała referencje do obiektów, a obiekty te nie byłyby nigdzie indziej używane, zostałyby oznaczone do usunięcia przez mechanizm odśmiecania. Dodatkowo po zajściu wyjątku blok try również przerywa działanie w miejscu wywołania metody quotient, więc wiersze od 26. do 28. nie wykonają się. Jeśli lokalne zmienne zostałyby utworzone w bloku try przed zgłoszeniem wyjątku, przestałyby istnieć.
Jeśli w wierszu 21. lub 23. pojawi się wyjątek InputMismatchException, blok try zakończy się i wykonywanie przejdzie do bloku catch z wierszy od 30. do 36. W takiej sytuacji metoda quotient nie zostanie w ogóle wywołana. Metoda main będzie kontynuowała działanie po ostatnim bloku catch.
Programowanie w Javie. Solidna wiedza w praktyce. Wydanie XI, Autorzy: Paul Deitel, Harvey Deitel, Wydawnictwo: Helion