PO Wyjątki: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Jsroka (dyskusja | edycje)
Jsroka (dyskusja | edycje)
Linia 405: Linia 405:
  '''class''' NieważnyWyjątek '''extends''' Exception {}  
  '''class''' NieważnyWyjątek '''extends''' Exception {}  
   
   
  '''public''' '''class''' ZaginiecieWyjatku {
  '''public''' '''class''' ZaginięcieWyjątku {
   '''static''' '''void''' niebezpiecznyKod() throws WażnyWyjątek, JakiśInnyWyjątek {
   '''static''' '''void''' niebezpiecznyKod() throws WażnyWyjątek, JakiśInnyWyjątek {
     '''throw''' '''new''' WażnyWyjątek();
     '''throw''' '''new''' WażnyWyjątek();

Wersja z 12:13, 28 lip 2006

Obsługa sytuacji wyjątkowych

Nawet w proprawnie napisanych programach należy się liczyć z możliwością wystąpienia sytuacji wyjątkowych. Najczęstrzym rodzajem błedów których nie można uniknąć jest niedostępność różnego rodzaju zasobów, np. niemożliwość zapisu do pliku dyskowego czy problemy z komunikacją przez sięć. Wiele problemów może się pojawić gdy nieumiejętnie korzystamy z kodu napisanego przez kogoś innego lub gdy inni nieumiejętnie korzystają z kodu napisanego przez nas. Wiąże się to zazwyczaj z niezaglądaniem do dokumentacji technicznej lub z jej słabą jakością. Typowym przykładem takiej sytuacji jest przekazanie metodzie niepoprawnych danych, np. wartości null gdy spodziewała się referencji do obiektu lub krótszej niż oczekiwała tablicy.

Dobrze napisany kod powinien zakładać możliwość wystąpienia takich wyjątkowych sytuacji. Zazwyczaj jednak nie da się podjąć żadnych działań naprawczych w miejscu wykrycia błędu, trzeba przerwać normalny tok działania programu i przekazać informacje o błędzie "na zewnętrz" do szerszego kontekstu (do metody, która wywołała metodę w której występiła sytuacja wyjątkowa lub jeszcze dalej), gdzie mogą być pojęte działania napracze. Starsze języki programowania jak C nie zawierały żadnych specjalnych mechanizmów ułatwiających radzenie sobie w takich sytuacjach. Istniał natomiast kilka ogólnie przyjętych schematów postępowania. Można zwracać specjalną wartość oznaczającą błąd lub ustawiać glabalnie dostępną flagę, którą następnie trzeba skontrolować. Z czasem okazało się jednak, że programiści nie byli konsekwentni w przestrzeganiu tych konwencji. Zakładali, że błędy zdarzają się jedynie w kodzie innych, a w ich nie. Często byli też po prostu leniwi, gdyż obsługa wszystkich nietypowych sytuacji, np. sprawdzanie czy błąd nie wystąpił po wywołaniu każdej kolejnej metody, powodowałaby wielokrotne napęcznienie kodu oraz drastycznie utrudniła jego stworzenie i zrozumienie. W efekcie sytuacje wyjątkowe nie były prawidłowo rozpoznawane i program wykonywał się dalej powodując wystąpienie jeszcze większych problemów. Brak specjanlnych mechanizmów wymuszających obsługę sytuacji wyjątkowych był ważnym ograniczeniem przy tworzeniu dużych, solidnych i łatwych w pielęgnowaniu programów.

Mechanizm obsługi wyjątków

Nowoczesne obiektowe języki programowania mają wbudowany specjalny mechanizm ułatwiający i upraszczający radzenie sobie z obsługą systuacji wyjątkowych. W chwili wykrycia takiej sytuacji można stworzyć specjalny obiekt nazywany wyjątkiem (ang. exception), zawrzeć w nim wszystkie informacje na temat tego co się stało i przy pomocy specjalnej instrukcji throw (w niktórych językach raise) zgłosić ten wyjątek do obsłużenia. Zgłoszenie wyjątku wymusza przerwanie normalnego trybu wykonywania programu i rozwinięcie stosu wywołań aż do napotkania kontekstu zawierającego kod obsługi dla wyjątków tego rodzaju. Jeżeli cały stos zostanie rozwinięty, a wyjątku nie obsłużono, program zostaje przerywany.

Nieobsłużone wyjątki

Metoda głębiej() w pokazanej poniżej klasie NieobsługiwanyWyjątek zgłasza wyjątek klasy Exception (podstawowa klasa reprezentująca wyjątki w Javie), gdy przekazany jej parametr ma wartość null.

public class NieobsługiwanyWyjątek {
  void głęboko(String s) throws Exception {
    System.out.println("początek głęboko");
    głębiej(s);
    System.out.println("koniec głęboko");
  }
  
  void głębiej(String s) throws Exception {
    System.out.println("początek głębiej");
    if (s == null) throw new Exception();
    System.out.println("koniec głębiej");
  }
  public static void main(String[] args) throws Exception {
    NieobsługiwanyWyjątek ue = new NieobsługiwanyWyjątek();
    System.out.println("przed głęboko");
    ue.głęboko(null);
    System.out.println("po głęboko");
  }
}

Z powodu zgłoszenia wyjątku stos wywołań jest rozwijany, a ponieważ żadna z kolejno znajdujących się tam metod głębiej(), głęboko() i main() nie zawiera kodu obsługi wyjątku, program zostaje przerwany. W takiej sytuacji Java wypisuje na standardowe wyjście błędu informacje o wyjątku, który spowodował przerwanie programu. Ich poziom szczegółowości zależy od ustawień kompilatora, ale przy ustawieniach domyślnych podawany jest ślad stosu wywołań z chwili wystąpienia wyjątku oraz numery linii w kodzie źródłowym odpowiadające kolejnym jego pozycjom.

przed głęboko
początek głęboko
początek głębiej
Exception in thread "main" java.lang.Exception
  at test.NieobsługiwanyWyjątek.głębiej(NieobsługiwanyWyjątek.java:12)
  at test.NieobsługiwanyWyjątek.głęboko(NieobsługiwanyWyjątek.java:6)
  at test.NieobsługiwanyWyjątek.main(NieobsługiwanyWyjątek.java:19)

Nadzorowanie obsługi wyjątków przez kompilator

Warto zwrócić uwagę, że w deklaracjach wszystkich metod klasy NieobsługiwanyWyjątek występuje klauzula throws, która informuje jakich wyjątków można się spodziewać w wyniku wywołania danej metody. Ta klauzula jest wymuszane przez kompilator, jeżeli z metody mogą wydostać sie jakieś nieobsłużone wyjątki. Dzięki temu programista nie ma możliwości przeoczyć żadnego wyjątku i jeżeli nie chce wszystkich obsłużyć musi świadomie wymienić je (bądź ich nadklasy) w deklaracji metody.

Obsługa wyjątków

try {
  //kod który może zgłosić wyjątki
} catch (Typ1 w) {
  //obsługa wyjątków Typ1
} catch (Typ2 w) {
  //obsługa wyjątków Typ2
} catch (Typ3 w) {
  //obsługa wyjątków Typ3
}

Do obsługi wyjątków służy instrukcja try-catch. Po try podaje się blok instrukcji, których wyjątki chcemy obsługiwać. Następnie następuje lista przypominających deklaracje metod klauzul catch, z których każda obsługuje wyjątki określonego typu. Typ wyjątku podaje się między nawiasami bezpośrednio po słowie kluczowym catch wraz z nazwą zmiennej, na którą zostanie przypisany złapany egzemplarz wyjątku. Do tej zmiennej można się odwoływać w kodzie obsługi wyjątku. Do obsługi wyjątku Wybierana jest zawsze pierwsza pasująca klauzula catch. Ponieważ wyjątki są obiektami "pasowanie" oznacza tu po prostu możliwość przypisania wyjątku na zmienną wymienioną po słowie kluczowym catch. Wszystkie dalsze klauzule catch są pomijane, nawet jeżeli pasowały. Jeżeli wyjątek nie pasuje do żadnej klauzuli catch, nie zostaje obsłużony i stos wywołań jest nadal rozwijany. W pokazanej poniżej klasie ObsługiwanyWyjątek, która jest modyfikacją poprzedniego przykładu, wyjątek Exception jest już obsługiwany i nie powoduje przerwania programu.

public class ObsługiwanyWyjątek {
  void głęboko(String s) {
    try {
      System.out.println("początek głęboko");
      głębiej(s);
      System.out.println("koniec głęboko");
    } catch (Exception e) {
      System.out.println("obsługa wyjątku");
      e.printStackTrace(System.out);
    }
    System.out.println("po obsłużeniu wyjątku");
  }

  void głębiej(String s) throws Exception {
    System.out.println("początek głębiej");
    if (s == null) throw new Exception();
    System.out.println("koniec głębiej");
  }

  public static void main(String[] args) {
    ObsługiwanyWyjątek ep = new ObsługiwanyWyjątek();
    System.out.println("przed głęboko");
    ep.głęboko(null);
    System.out.println("po głęboko");
  }
}

Warto zwrócić uwagę, że metody głęboko() i main() nie muszą już posiadać klauzuli throws.

Na konsolę nadal wypisywane są informacje o wyjątku, ale jest to spowodowane użyciem w kodzie obsługi wyjątku metody printStackTrace() z parametrem System.out. Metoda ta jest odziedziczona z klasy Throwable, po której muszą dziedziczyć wszystkie wyjątki.

przed głęboko
początek głęboko
początek głębiej
obsługa wyjątku
java.lang.Exception
  at test.ObsługiwanyWyjątek.głębiej(ObsługiwanyWyjątek.java:18)
  at test.ObsługiwanyWyjątek.głęboko(ObsługiwanyWyjątek.java:7)
  at test.ObsługiwanyWyjątek.main(ObsługiwanyWyjątek.java:25)
po obsłużeniu wyjątku
po głęboko

Hierarchia wyjątków w Javie

W trakcie wykonywania instrukcji występujących po słowie kluczowym try mogą zajść różne sytuacje wyjątkowe. Być może na każdą z nich należy zareagować w inny sposób. W obiekcie zgłaszanym jako wyjątek można zawrzeć dodatkowe informacje pozwalające zdecydować jakie działania naprawcze podjąć. Zazwyczaj jednak używana się wyjątków będących egzemplarzami różnych klas i stosuje dla każdej z nich oddzielną klauzulę catch. W Javie zdefiniowanych jest wiele typowych wyjątków, najważniejsze w pakiecie java.lang. Są one używane przez klasy ze standardowych bibliotek. Różnią się jedynie nazwą, która zazwyczaj pozwala zorientować się co dany wyjątek oznacza. W swoich aplikacjach można korzystać z tych klas lub zdefiniować nowe, pasujące do konkretnego zastosowania. Wszystkie klasy, które mogą być zgłoszone jako wyjątek muszą dziedziczyć po klasie Throwable. Tej klasy jednak bezpośrednio się nie rozszerza. Jej najważniejsze podklasy są pokazane poniżej.

java.lang.Object
  |-java.lang.Throwable
      |-java.lang.Error
      |   |-java.lang.VirtualMachineError
      |   ...
      |-java.lang.Exception
          |-java.io.IOException
          ...
          |-java.lang.RuntimeException
              |-java.lang.ClassCastException
              |-java.lang.IndexOutOfBoundsException
              |-java.lang.NullPointerException
              ...

Klasa Error

Jedyne dwie bezpośrednie podklasy Throwable to Error i Exception. Wyjątki dziedziczące po Error reprezentują poważne problemy, których aplikacja nie będzie w stanie rozwiązać. Przykładową podklasą jest VirtualMachineError, która oznacza, że maszyna wirtualna nie może dalej kontynuować pracy, bo np. wyczerpały się zasoby systemowe. Wyjątków rozszerzających Error nie należy przechwytywać, gdyż nie ma możliwości zaradzenia sytuacjom wyjątkowym, które je spowodowały. Z założenia mogą wystąpić praktycznie w każdej instrukcji kodu i nie muszą być wymieniane w klauzulach throws.

Klasa Exception

Wyjątki dziedziczące po Exception reprezentują sytuacje, na które dobrze napisana aplikacja powinna być przygotowana. To właśnie tą klasę rozszerza się tworząc własne rodzaje wyjątków. Jej przykładową podklasą jest IOException, która reprezentuje sytuacje wyjątkowe związane z wejściem/wyjściem.

Klasa RuntimeException

Bardzo ciekawą podklasą Exception jest RuntimeException, która sama posiada wiele podklas. Wyjątki rozszerzające RuntimeException mogą wystąpić podczas typowych operacji jak rzutowanie zmiennej, odwołanie się do elementu tablicy lub odwołanie się do składowej obiektu. Ich wystąpienie zazwyczaj oznacza, że programista popełnił błąd w swoim kodzie lub nieumiejętnie korzystał z kodu napisanego przez innych. Maszyna wirtualna wykrywa wystąpienie takich błędów w trakcie działania programu i informuje o tym zgłaszając odpowiedni wyjątek. Przykładowymi podklasami RuntimeException są:

  • ClassCastException – oznacza próbę rzutowania zmiennej na niepasujący typ,
  • IndexOutOfBoundsException– oznacza odwołanie się do indeksu z poza zakresu oraz
  • NullPointerException – oznacza że zamiast referencji wskazującej na obiekt pojawiła się wartość null.

Wyjątki te można obsługiwać, chociaż zdecydowanie lepiej jest zadbać żeby się nie pojawiały. Zgłaszając ten typ wyjątków lub tworząc nowe ich rodzaje pamiętaj, że wskazują błędy w programie, a nie nietypowe sytuację. Ze względu na swoją wszechobecność wymienianie ich w klauzulach throws praktycznie wszystkich metod mijałoby się się z celem, więc tak samo jak w przypadku wyjątków rozszerzających Error nie ma takiego obowiązku.

Rozszerzanie klasy Exception

Klasę Exception rozszerza się tak jak każdą inną klasę. Jak już zostało powiedziane rozróżnianie typu wyjątku wystarcza w większości zastosowań, dlatego zazwyczaj nie definiuje się ani nie przedefiniowuje się żadnych metod.

class MójNowyWyjątek1 extends Exception {}

public class NowyWyjątek1 {
  public static void main(String[] args) {
    try {
      System.out.println("Rozpoczynamy test nowego wyjątku.");
      throw new MójNowyWyjątek1();
    } catch (MójNowyWyjątek1 w) {
      System.out.println("Wiwat twórca wyjątków!");
    }
  }
}

Są jednak sytuacje, gdy warto w wyjątku przekazać jakieś dodatkowe informacje. Czasami wyjątki są zgłaszane, aby przenieść tekstowy opis nietypowej sytuacji, aby mógł być przedstawiony użytkownikowi przez kod obsługi wyjątku. Przykładem takiej sytuacji jest niemożliwość odnalezienia lub otwarcia pliku, którego nazwę wskazał użytkownik. Dlatego wszystkie wyjątki z pakietu java.lang (w tym Exception) posiadają dodatkowy konstruktor przyjmujący jako parametr napis przenoszony przez wyjątek. Oprócz tego, nadklasa wszystkich wyjątków Throwable, definiuje metodę getMessage() przy pomocy której można ten napis z wyjątku wydobyć. Żeby móc korzystać z tego mechanizmu należy do swoich wyjątków dodać konstruktory przyjmujący napis.

class MójNowyWyjątek2 extends Exception {
  MójNowyWyjątek2(String s) {
    super(s);
  }

  //ponieważ definiujemy jakiś inny konstruktor,
  //kompilator sam nie doda bezparametrowego
  MójNowyWyjątek2() {}
}

public class NowyWyjątek2 {
  public static void main(String[] args) {
    try {
      throw new MójNowyWyjątek2("Ten napis przyda się podczas obsługi wyjątku.");
    } catch (MójNowyWyjątek2 w) {
      System.out.println(w.getMessage());
    }
  }
}

Zgłaszanie wyjątków w klauzuli catch

Wyjątki zgłoszone w klauzuli catch nie są obsługiwane przez tą samą instrukcję try-catch.

Ponowne zgłaszanie obsługiwanego wyjątku

Czasami obsługiwany wyjątek jest zgłaszany ponownie.

void głębiej() throws Exception {
  throw new Exception();
}

void głęboko() throws Exception {
  try {
    głębiej();
  } catch (Exception e) {
    throw e;
  }
}

Ponowne zgłoszenie nie zmienia zapamiętanego śladu wywołań. Nie jest on wypełniany w chwili zgłoszenia wyjątku, ale w chwili jego utworzenia. Dzieje się tak za sprawą konstruktora z nadklasy Throwable, który wywołuje metodę fillInStackTrace(). Przy pomocy tej metody można samemu podmienić na aktualny ślad stosu wywołań w ponownie zgłaszanym wyjątku.

void głębiej() throws Exception {
  throw new Exception();
}

void głęboko() throws Exception {
  try {
    głębiej();
  } catch (Exception e) {
    e.fillInStackTrace();
    throw e;
  }
}

Wyjątek powodujący zgłoszenie innego wyjątku

Od Javy 1.4 do zgłaszanego wyjątku można dołączyć wyjątek, który spowodował jego wystąpienie. Używa się do tego metody initCause(), która jako parametr przyjmuje dołączany wyjątek. Dla wygody do klas Throwable, Exception i RuntimeException dodano również po dwa nowe konstruktory. Pierwszy przyjmujący jako jedyny parametr wyjątek do dołączenia oraz drugi przyjmujący jako pierwszy parametr napis przenoszony przez wyjątek oraz jako drugi parametr wyjątek do dołączenia. Jeżeli mamy zamiar w ten sposób łączyć wyjątki w ciągi, warto dodać takie konstruktory do nowo definiowanych wyjątków.

class MójNowyWyjątek extends Exception {
  public MójNowyWyjątek() {
    super();
  }

  public MójNowyWyjątek(String message) {
    super(message);
  }

  public MójNowyWyjątek(Throwable cause) {
    super(cause);
  }

  public MójNowyWyjątek(String message, Throwable cause) {
    super(message, cause);
  }
}

public class WyjątekSpowodowanyWyjątkiem {
  public static void main(String[] args) throws MójNowyWyjątek {
    try {
      throw new Exception("To ja jestem wszystkiemu winien.");
    } catch (Exception e) {
      throw new MójNowyWyjątek("To nie moja wina.", e);
    }
  }
}

Informacje o dołączonym wyjątku można uzyskać przy pomocy metody getCause(). Informacje te są również wypisywane na standardowe wyjście błędu, jeżeli wykonanie programu zostało przerwane przez nieobsłużony wyjątek.

Exception in thread "main" test.MójNowyWyjątek: To nie moja wina.
  at test.WyjątekSpowodowanyWyjątkiem.main(WyjątekSpowodowanyWyjątkiem.java:26)
Caused by: java.lang.Exception: To ja jestem wszystkiemu winien.
  at test.WyjątekSpowodowanyWyjątkiem.main(WyjątekSpowodowanyWyjątkiem.java:24)

Wsparcie ze strony kompilatora

Wiele błędów w programie związanych z nieprawidłową obsługą wyjątków można wykryć już w chwili kompilacji. Jak wyjaśniliśmy wcześniej kompilator pilnuje, aby wszystkie potencjalne wyjątki zostały obsłużone lub jawnie wymienione w nagłówku metody. Co więcej kompilator nie pozwoli umieścić nieosiągalnej klauzuli catch. Klauzula jest nieosiągalna, jeżeli wszystkie potencjalne wyjątki do niej pasujące są obsługiwane przez wcześniejsze klauzule

public class NieosiągalnaKlauzulaCatch {
  public static void main(String[] args) {
    try {
      System.out.println("Kompilator na ratunek gapom.");
    } catch (Exception e) {
      System.out.println("Ta klauzula nic nie przepuszcza dla pozostałych.");
    } //catch (NullPointerException e) {
      //System.out.println("Ten kod jest tu niepotrzebny.");
    //}
  }
}

Oprócz tego dana klauzula catch może być po prostu niepotrzebna, jeżeli nigdzie w bloku try nie jest zgłaszany wyjątek do niej pasujący ani nie jest używana żadna metoda, która by taki wyjątek miała wymieniony w swojej klauzuli throws. Wystąpienie nieosiągalnej lub niepotrzebnej klauzuli catch oznacza, że programista się pomylił i chciał zrobić coś innego dlatego kompilator zwróci na to uwagę.

Kolejnym rodzajem pomyłek przed, którymi kompilator chroni nieuważnych programistów jest nieosiągalny fragment kodu znajdujący się po zgłaszanym wyjątku.

throw new Exception();
//System.out.println("Ten kod nigdy by się nie wykonał.");

Zwalnianie zasobów

W Javie programista nie musi martwić się o rezerwowanie i zwalnianie pamięci. Odbywa się to automatycznie. Mimo to o rezerwowanie i zwalnianie wielu innych zasobów trzeba dbać samemu. Najbardziej typowe przykłady to otwarte pliki, połączenia sieciowe oraz połączenia z bazą danych. Pilnowanie aby wszystkie zasoby zostały w końcu zwolnione wymaga wiele dyscypliny, a jak nie trudno sobie wyobrazić możliwość wystąpienia wyjątku wcale nie ułatwia tego zadania. Wyjątki występujące w trakcie czytania z pliku, przesyłania danych przez sieć lub korzystania z bazy danych wcale nie są czymś nadzwyczajnym.

W poniższym przykładzie używamy hipotetycznego zasobu, który najpierw trzeba zarezerwować a potem zwolnić. W między czasie wykonywane są niebezpieczne operacje. Wobec tego w każdej klauzuli catch musimy pilnować, żeby mimo wystąpienia wyjątku zasób został zwolniony. Co więcej mimo, że w tej instrukcji try-catch chcieliśmy obsługiwać tylko dwa wyjątki IOException oraz InnyMożliwyWyjątek, wyłapujemy również wszystkie inne, żeby zanim je ponownie zgłosimy zadbać o zwolnienie naszego zasobu.

import java.io.IOException;

class InnyMożliwyWyjątek extends Exception {}

class Zasób {
  void zarezerwuj() {}
  void używaj() throws IOException {}
  void innaNiebezpiecznaOperacja() throws InnyMożliwyWyjątek {}
  void zwolnij() {}
}

public class ZwalnianieZasobów1 {
  public static void main(String[] args) throws Exception {
    Zasób z = new Zasób();
    try {
      z.zarezerwuj();

      //tu jest niebezpieczny kod
      z.używaj();
      z.innaNiebezpiecznaOperacja();
      //...

      //zwalnianie zasobów
      z.zwolnij();
    } catch (IOException e) {
      //obsługa wyjątku IOException

      //zwalnianie zasobów
      z.zwolnij();
    } catch (InnyMożliwyWyjątek w) {
      //obsługa wyjątku InnyMożliwyWyjątek

      //zwalnianie zasobów
      z.zwolnij();
    } catch (Exception e) {
      //innych wyjątków nie obsługujemy,
      //ale przechwytujemy je na chwilę, żeby zwolnić zasoby

      //zwalnianie zasobów
      z.zwolnij();
      throw e;
    }
  }
}

Takie rozwiązanie jest pracochłonne i wymaga dużo uwagi. Kod wykonujący operacje na zasobie może być skomplikowany. Zwalnianie zasobów może być konieczne w kilku miejscach, np. wszędzie przed wystąpieniem instrukcji return kończącej wykonanie metody, która zawiera rozważaną przez nas instrukcję try-catch oraz (jeżeli instrukcja ta była w pętli) wszędzie przed wystąpieniem instrukcji break i continue. Oba te przypadki pokazuje poniższy przykład.

import java.io.IOException;

class InnyMożliwyWyjątek extends Exception {}

class Zasób {
  Zasób(int i) {
    //...
  }
  void zarezerwuj() {}
  void używaj() throws IOException {}
  void innaNiebezpiecznaOperacja() throws InnyMożliwyWyjątek {}
  void zwolnij() {}
}

public class ZwalnianieZasobów2 {
  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 10; i++) {
      Zasób z = new Zasób();
      try {
        z.zarezerwuj();

        //tu jest niebezpieczny kod
        z.używaj();
        z.innaNiebezpiecznaOperacja();
        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //trzeba zwolnić zasób, bo kończy się obrót pętli
        if (i == 3) continue;
        //trzeba zwolnić zasób, bo kończy się cała metoda
        if (i == 8) return;
        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //...

        //zwalnianie zasobów
        z.zwolnij();
      } catch (IOException e) {
        //obsługa wyjątku IOException

        //zwalnianie zasobów
        z.zwolnij();
      } catch (InnyMożliwyWyjątek w) {
        //obsługa wyjątku InnyMożliwyWyjątek

        //zwalnianie zasobów
        z.zwolnij();
      } catch (Exception e) {
        //innych wyjątków nie obsługujemy,
        //ale przechwytujemy je na chwilę, żeby zwolnić zasoby

        //zwalnianie zasobów
        z.zwolnij();
        throw e;
      }
    }
  }
}

W niektórych językach programowania, np. w C++, aby ułatwić zadanie, do obiektów dodawana jest specjalna metoda tak zwany destruktor, która jest wykonywana w chwili niszczenia obiektu. Jest to doskonałe miejsce na umieszczenie kodu zwalniającego zasoby. W C++ destruktory są bardzo często używane, gdyż programista jest odpowiedzialny również za zwalnianie pamięci. W Javie zajmuje sie tym odśmiecacz, który wprawdzie wywołuje na każdym obiekcie metodę finalize() (można by próbować zwalniać w niej zasoby), ale nie ma gwarancji, że obiekty zostaną poddane procesowi odśmiecania. Może się zdarzyć, że przez cały czas działania programu pamięci nie brakowało. Java oferuje inny mechanizm rekompensujący brak destruktora.

Klazula finally

Do instrukcji try można dodać klauzulę finally. Zawarty w niej kod jest wykonywany zawsze, niezależnie od tego czy w bloku try jest zgłaszany wyjątek czy nie (i niezależnie od tego czy try kończy się normalnie, z powodu return, czy z powodu break lub continue). Klauzulę finally umieszcza się bezpośrednio po ostatniej klauzuli catch.

try {
  //kod który może zgłosić wyjątki
} catch (Typ1 w) {
  //obsługa wyjątków Typ1
} catch (Typ2 w) {
  //obsługa wyjątków Typ2
} catch (Typ3 w) {
  //obsługa wyjątków Typ3
} finally {
  //kod wykonywany niezależnie od wystąpienia wyjątku
}

Można też użyć samej klauzuli finally i nie obsługiwać żadnych wyjątków.

try {
  //kod który może zgłosić wyjątki
} finally {
  //kod wykonywany niezależnie od wystąpienia wyjątku
}

Jeżeli w bloku try nie wystąpił wyjątek, kod z klauzuli finally wykonywany jest bezpośrednio po jego zakończeniu. Jeżeli wyjątek wystąpił, ale nie pasuje do żadnej klauzuli catch lub żadna klauzula catch nie została użyta, kod z klauzuli finally wykonywany jest bezpośrednio po wystąpieniu wyjątku, a przed rozpoczęciem rozwijania stosu wywołań w poszukiwaniu innej instrukcji try-catch, która mogłaby ten wyjątek obsłużyć. W końcu, jeżeli wyjątek wystąpił i jest pasująca klauzula catch, to kod z klauzuli finally wykonywany jest bezpośrednio po zakończeniu obsługi wyjątku. Opisaną kolejność obrazuje poniższy przykład.

public class TestFinally {
  public static void main(String[] args) {
    try {
      System.out.println("Zewnętrzne try");
      try {
        System.out.println("Pierwsze wewnętrzne try");
      } finally {
        System.out.println("Pierwsze wewnętrzne finally");
      }
      try {
        System.out.println("Drugie wewnętrzne try");
        throw new Exception();
      } finally {
        System.out.println("Drugie wewnętrzne finally");
      }
    } catch (Exception e) {
      System.out.println("Obsługa wyjątku");
    } finally {
      System.out.println("Zewnętrzne finally");
    }
  }
}

W wyniku działania tego programu na standardowym wyjściu zostanie wyprodukowany następujący wynik.

Zewnętrzne try
Pierwsze wewnętrzne try
Pierwsze wewnętrzne finally
Drugie wewnętrzne try
Drugie wewnętrzne finally
Obsługa wyjątku
Zewnętrzne finally

Zaginięcie wyjątku

Z użyciem finally wiąże się jeden mankament. Kod zawarty w tej klauzuli w żadnym wypadku nie powinien zgłaszać wyjątku. Jeżeli wyjątek zgłaszany w bloku try nie został obsłużony, a w finally również zgłaszany jest wyjątek, to wyjątek z bloku try zaginie. Tak właśnie dzieje się w poniższym przykładzie.

class WażnyWyjątek extends Exception {}
class JakiśInnyWyjątek extends Exception {}
class NieważnyWyjątek extends Exception {} 

public class ZaginięcieWyjątku {
  static void niebezpiecznyKod() throws WażnyWyjątek, JakiśInnyWyjątek {
    throw new WażnyWyjątek();
  }
  public static void main(String[] args) throws Exception {
    try {
      //tego wyjątku nie wolno przegapić
      niebezpiecznyKod();
    } catch (JakiśInnyWyjątek w) {
      //obsługa jakiegoś innego wyjątku
    } finally {
      //zwalnianie zasobów
      throw new NieważnyWyjątek();
    }
  }
}

Zawężanie wyjątków przy rozszerzaniu klas

Wyjątki zgłaszane przy inicjalizacji obiektów

Anotacje