Wyjątki – „Pamiętaj cholero, nie dziel przez zero”

AUTOR: MAREK SOWA

 

Wyjątki w Javie to specjalne obiekty, które pozwalają obsłużyć nietypowe sytuacje. Sytuacje te powodują zwykle zatrzymanie programu, chyba że zostaną odpowiednio przechwycone. Po przechwyceniu takiej nietypowej sytuacji, program może wrócić do "normalnego" działania. Dobrym przykładem zobrazowania wyjątku może być koło zapasowe w samochodzie. Odpowiedzialny kierowca wozi je ze sobą. W przypadku złapania "gumy", może wymienić koło i jechać dalej - można by rzec, że kierowca obsłużył tę wyjątkową sytuację.

 

Obsługa wyjątków

Obsługiwanie wyjątków to nic innego jak naprawianie programu, gdy w trakcie jego działania wydarzy się coś, co nie powinno, ale mogło i się wydarzyło.
Aby poprawnie przygotować się do wyjątkowej sytuacji, potrzeba kilku rzeczy: wskazania miejsca, w którym może do niej dojść  oraz napisania instrukcji awaryjnej na wypadek wystąpienia takiego zdarzenia. 

try {
    // miejsce, w którym może wystąpić wyjątek
} catch ( /*typ wyjątku*/) {
    // instrukcja, która zostanie wykonana w przypadku nastąpienia wyjątku
} finally {
    // instrukcja, która wykona się niezależnie czy wystąpi wyjątek, czy też nie
}

 

Nawiązując do naszej sytuacji z samochodem, nasz kod wyglądałby tak:

try {
    rideCar();
} catch (flatTyreException e) {
    stopCar();
    replaceTyre();
} finally {
    checkSeatBelts();
}

W bloku try sprawdzamy sytuację w której może dojść do złapania gumy. Jeżeli podczas prowadzenia samochodu - czyli instrukcja rideCar() - dojdzie do uszkodzenia opony, to program sprawdzi typ wyjątku poprzez catch. Jeżeli wyjątek jest przewidzianego typu, w naszym wypadku flatTyreException, to catch przychwyci ten typ wyjątku i wykona się instrukcja w tym konkretnym bloku, czyli stopCar() oraz replaceTyre(). Niezależnie czy uszkodziła nam się opona, czy też nie, na koniec sprawdzimy zapięte pasy: metoda checkSeatBelts(), bo zadeklarowana została instrukcja finally. Finally wykona się zawsze po bloku catch albo po bloku try jeżeli nie wystąpił po nim wyjątek.

Do jednej sprawdzanej sytuacji można próbować przechwytywać wiele typów wyjątków deklarując je kolejno pod spodem w osobnych blokach catch:

try {
    startEngine();
} catch (emptyFuelTankException e) {
    callDad();
} catch (dischargeBatteryException b) {
    callDad();
} catch (disconnectingCableException c) {
    callDad();
} catch (alarmException a) {
    runAsFastAsYouCan();
}

Należy pamiętać, że każda deklaracja try, czyli przewidywanie wyjątkowych sytuacji, jest bardzo kosztowna dla naszego procesora. Za każdym razem procesor będzie sprawdzał, czy nie zaistniała ta wyjątkowa sytuacja. Starajmy się przewidywać tylko takie wyjątki, które mają realną szansę wystąpienia w danym momencie i mogą spowodować realne zagrożenie dla dalszego ciągu programu. Wyobraźcie sobie szosę, na której co metr stał by znak ostrzegawczy pokazujący wybuchające wulkany. Teoretycznie jest szansa, że wybuchnie wulkan i będzie to wyjątkowa sytuacja, ale chyba nie trzeba co chwilę informować o tym kierowców. Prawdopodobieństwo jest znikome i jak już do niej dojdzie, to mówi się trudno. Budowa schronów przeciw erupcyjnych przy drogach była by w tej skali nieekonomiczna. Podobnie sytuacje takie jak: meteoryty,  zawalone drzewa czy brak dziur na jezdni.

Deklaracja bloku try nie może być samodzielna, po deklaracji bloku try zawsze musimy użyć bloku catch albo finally.

* blok finally nie wykona się, gdy w bloku try napiszemy instrukcję kończącą program

** również, gdy zabraknie prądu =]

 

Hierarchia wyjątków

W Javie wszystko dziedziczy po obiekcie. Prawdą jest też stwierdzenie, że podstawowe klasy dziedziczą po Exception:

Wyjatki2

Klasa Throwable jest wyznacznikiem, powoduje, że klasy po niej dziedziczące mogą "rzucać" obiektami (throw). Klasa Error jest odpowiedzialna za wyrzucanie błędów i korzysta z niej głównie JVM sygnalizując nam o przepełnieniu stosu albo braku dostępnej pamięci. Początkujący programista powinien wiedzieć, że klasa Exception dzieli się na dwa typy, które omówimy poniżej.

 

Checked Exceptions. Wyjątki, które trzeba obsłużyć

Ten rodzaj wyjątków został zaznaczony niebieską obwódką na wcześniejszym obrazku. Ten typ wyjątków jest zawsze weryfikowany już na etapie pisania. Większość IDE już wtedy wymusi na programiście obsługę tego typu wyjątków i nie pozwoli przejść dalej, ponieważ wymaga tego kompilator. Jest to zabezpieczenie przed wyjątkami, po których powrót do normalnego działania programu byłby trudny bądź wręcz niemożliwy. Przykładem są wyjątki typu wejścia/wyjścia IOException, występujące na przykład podczas transmisji danych za pośrednictwem połączenia internetowego albo przy pracy na plikach:

try{
    FileInputStream i = new FileInputStream(new File("jakisplik"));
    i.read();
} catch (FileNotFoundException e){
    e.printStackTrace();
} catch (IOException e){
    e.printStackTrace();
} finally {
    System.out.println("to wykona sie na 100%");
}

 

Unchecked Exceptions. Wyjątki, których nie musimy obsługiwać

Ten rodzaj wyjątków został zaznaczony pomarańczową obwódką na wcześniejszym obrazku. Są to bardzo popularne wyjątki wśród młodych programistów, zwłaszcza ukochany NullPointerException albo ArrayIndexOutOfBoundsExceptionKompilator nie wymusza ich przechwytywania, ponieważ mogą występować bardzo często w programie. Pamiętacie, że sprawdzanie wyjątków jest mocno kosztowne dla komputera oraz dbajmy o wygląd programu, by nie dopuścić do sytuacji, w której co metr jest znak ostrzegawczy. Dlatego programiści pisząc kod, powinni mieć się na baczności i starać się napisać go tak, by nie trzeba było sprawdzać wyjątkowych sytuacji:

int intArray[] = {1,2,3,4,5};
System.out.println("Ostatni element to: " + intArray[5]); // java.lang.ArrayIndexOutOfBoundsException: 5

 

Wiele tego typu wyjątków można wyeliminować zwykłym if, na etapie metod lub używać alternatywnych rozwiązań:

int intArray[] = {1,2,3,4,5};
System.out.println("Ostatni element to: " + intArray[intArray.length-1]); // Ostatni element to: 5

 

Rzucanie wyjątków  (throw)

Pisząc kod, prędzej czy później będziecie chcieli, by program poinformował was, co poszło nie tak, czyli zgłosił wyjątek. Zgłosić wyjątek możemy poprzez rzucenie go, throw:

public int divide(int a, int b){
    if(b==0){
        throw new IllegalArgumentException("do not divide by 0");
    }
    return a/b;
}

 

W przykładzie dzielenia powyżej, sprawdzamy, czy ktoś nie chce podzielić przez zero, a zgodnie ze starym przysłowiem :

Pamiętaj cholero, nie dziel przez zero.

 

Nie możemy pozwolić, by taka sytuacja miała miejsce. Jeżeli już do niej dojdzie, to dobrze by było wiedzieć, jaki typ wyjątku zostanie rzucony, by później można go było obsłużyć. W naszym powyższym przykładzie jest to typ IllegalArgumentException, który można ładnie obsłużyć:

    //kod programu
    try {
        divide(2, 0);
    }catch (IllegalArgumentException e){
        e.printStackTrace(); // do not divide by 0
        System.out.println("cholero! dzielisz przez zero!");
    } finally {
        System.out.println("to wykona sie na 100%");
    }
}

//metoda dzielenia
public int divide(int a, int b){
    if(b==0){
        throw new IllegalArgumentException("do not divide by 0"); 
    }
    return a/b;
}

 

Na powyższym przykładzie widzimy, że operację dzielenia zapisujemy w bloku try, ponieważ w niej może dojść do wystąpienia wyjątku. Jeżeli dojdzie, to przechwytujemy typ wyjątku, w naszym wypadku jest to IllegalArgumentException, ponieważ taki zadeklarowaliśmy w metodzie divide(). Pierwszą instrukcją po przechwyceniu tego typu wyjątku jest metoda printStackTrace(), czyli wydrukowanie naszej informacji: "do not divide by 0" a kolejną instrukcją jest wydrukowanie do konsoli: "cholero! nie dziel przez zero!".

 

Klauzula throws

Kolejną rzeczą wartą zapamiętania jest słówko throws, informujące, że w metodzie może wystąpić wyjątek. Jest to bardzo przydatne, gdy pracujemy w zespole, możemy w prosty sposób poinformować innych programistów o możliwości wystąpienia wyjątku:

try {
    readFile(new File("sample.txt"));
}catch (IOException e){
    System.out.println("wyjątek przechwycony");
}


public void readFile(File file) throws IOException{
    throw new IOException("file error");
}

 

* klauzulę throws dołączamy w przypadku możliwości wystąpienia wyjątków dziedziczących po IOException czyli checked.