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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Aneczka (dyskusja | edycje)
Nie podano opisu zmian
Jsroka (dyskusja | edycje)
 
(Nie pokazano 31 wersji utworzonych przez 3 użytkowników)
Linia 1: Linia 1:
{{powrot|Programowanie obiektowe|}}
{{powrot|Programowanie obiektowe|do przedmiotu Programowanie obiektowe}}


== Zwalnianie zasobów ==
== 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.
W Javie programista nie musi martwić się o rezerwowanie i zwalnianie pamięci. Odpowiada za to odśmiecacz. 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 możliwość wystąpienia wyjątku - jak nie trudno sobie wyobrazić - wcale nie ułatwia tego zadania. Niestety, 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ć. Pomiędzy tymi czynnościami wykonywane są niebezpieczne operacje. Wobec tego w każdym bloku '''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 zadbać o zwolnienie naszego zasobu zanim je ponownie zgłosimy.
  '''import''' java.io.IOException;
  '''import''' java.io.IOException;
   
   
Linia 41: Linia 41:
       z.zwolnij();
       z.zwolnij();
     } '''catch''' (Exception e) {
     } '''catch''' (Exception e) {
       //innych wyjątków nie obsługujemy,
       //innych wyjątków nie obsługujemy, ale przechwytujemy je na chwilę, żeby zwolnić zasoby
      //ale przechwytujemy je na chwilę, żeby zwolnić zasoby
      //złapią się tu również wyjątki niekontrolowane (rozszerzające RuntimeException)
   
   
       //zwalnianie zasobów
       //zwalnianie zasobów
Linia 50: Linia 50:
   }
   }
  }
  }
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.
Takie rozwiązanie jest pracochłonne i wymaga dużo uwagi. Kod wykonujący operacje na zasobie może być skomplikowany. Trzeba pamiętać o wyjątkach niekontrolowanych oraz o wszystkich możliwościach opuszczenia bloku '''try'''. Zwolnienie zasobów może być np. konieczne przed każdym 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'''. Te przypadki pokazuje poniższy przykład.
  '''import''' java.io.IOException;
  '''import''' java.io.IOException;
   
   
Linia 56: Linia 56:
   
   
  '''class''' Zasób2 {
  '''class''' Zasób2 {
   Zasób2(int i) {
   Zasób2('''int''' i) {
     //...
     //...
   }
   }
Linia 67: Linia 67:
  '''public''' '''class''' ZwalnianieZasobów2 {
  '''public''' '''class''' ZwalnianieZasobów2 {
   '''public''' '''static''' '''void''' main(String[] args) '''throws''' Exception {
   '''public''' '''static''' '''void''' main(String[] args) '''throws''' Exception {
     '''for''' (int i = 0; i < 10; i++) {
     '''for''' ('''int''' i = 0; i < 10; i++) {
       Zasób2 z = '''new''' Zasób2(i);
       Zasób2 z = '''new''' Zasób2(i);
       '''try''' {
       '''try''' {
Linia 96: Linia 96:
         z.zwolnij();
         z.zwolnij();
       } '''catch''' (Exception e) {
       } '''catch''' (Exception e) {
         //innych wyjątków nie obsługujemy,
         //innych wyjątków nie obsługujemy, ale przechwytujemy je na chwilę, żeby zwolnić zasoby
        //ale przechwytujemy je na chwilę, żeby zwolnić zasoby
        //złapią się tu również wyjątki niekontrolowane (rozszerzające RuntimeException)
   
   
         //zwalnianie zasobów
         //zwalnianie zasobów
Linia 107: Linia 107:
  }
  }


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.
W niektórych językach programowania, np. w C++, aby ułatwić zadanie, do obiektów dodawana jest specjalna metoda &ndash; 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 się tym odśmiecacz, który wprawdzie wywołuje metodę ''finalize()'' każdego zwalnianego obiektu (można by próbować zwalniać w niej zasoby), ale nie ma gwarancji, że obiekty zostaną poddane procesowi odśmiecania. Może się przecież zdarzyć, że przez cały czas działania programu pamięci nie brakowało. Java oferuje inny mechanizm rekompensujący brak destruktora.


=== Klazula '''finally''' ===
=== Blok '''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'''). Klauzula '''finally''' jest doskonałym miejscem na zwalnianie wszelkiego rodzaju zasobów. Umieszcza się bezpośrednio po ostatniej klauzuli '''catch'''.
Do instrukcji '''try''' można dodać blok '''finally'''. Zawarty w nim 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'''. Blok '''finally''' jest doskonałym miejscem na zwalnianie wszelkiego rodzaju zasobów. Umieszcza się go bezpośrednio po ostatnim bloku '''catch'''.
  '''try''' {
  '''try''' {
   //kod który może zgłosić wyjątki
   //kod który może zgłosić wyjątki
Linia 121: Linia 121:
  } '''finally''' {
  } '''finally''' {
   //kod wykonywany niezależnie od wystąpienia wyjątku
   //kod wykonywany niezależnie od wystąpienia wyjątku
  //tu zwalniamy zasoby
  }
  }
Można też użyć samej klauzuli '''finally''' i nie obsługiwać żadnych wyjątków.
Można też użyć samego bloku '''finally''' i nie obsługiwać żadnych wyjątków.
  '''try''' {
  '''try''' {
   //kod który może zgłosić wyjątki
   //kod który może zgłosić wyjątki
  } '''finally''' {
  } '''finally''' {
   //kod wykonywany niezależnie od wystąpienia wyjątku
   //kod wykonywany niezależnie od wystąpienia wyjątku
  //tu zwalniamy zasoby
  }
  }
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.
Jeżeli w bloku '''try''' nie wystąpił wyjątek, kod z bloku '''finally''' wykonywany jest bezpośrednio po jego zakończeniu. Jeżeli wyjątek wystąpił, ale nie pasuje do żadnego bloku '''catch''' lub nie umieszczono żadnego bloku '''catch''', kod z bloku '''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ący blok '''catch''', to kod z bloku '''finally''' wykonywany jest bezpośrednio po zakończeniu obsługi wyjątku. Opisaną kolejność obrazuje poniższy przykład.
  '''public''' '''class''' TestFinally {
  '''public''' '''class''' TestFinally {
   '''public''' '''static''' '''void''' main(String[] args) {
   '''public''' '''static''' '''void''' main(String[] args) {
Linia 161: Linia 163:


=== Zaginięcie wyjątku ===
=== 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.
Z użyciem '''finally''' wiąże się jeden mankament. Kod zawarty w tym bloku 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''' WażnyWyjątek '''extends''' Exception {}
  '''class''' JakiśInnyWyjątek '''extends''' Exception {}
  '''class''' JakiśInnyWyjątek '''extends''' Exception {}
Linia 179: Linia 181:
       //zwalnianie zasobów
       //zwalnianie zasobów
       '''throw''' '''new''' NieważnyWyjątek();
       '''throw''' '''new''' NieważnyWyjątek();
    }
  }
}
=== Try z zasobami ===
Klauzula '''finally''' ułatwia nam bezpieczne gospodarowanie zasobami, ale nadal zrobienie tego poprawnie jest pracochłonne i wymaga skupienia uwagi. W kolejnym przykładzie dodatkowo dopuścimy powstawanie wyjątków podczas inicjalizacji obiektu reprezentującego zasób oraz podczas jego zwalniania.
'''class''' Zasób3 {
  Zasób3() '''throws''' IOException {}
  '''void''' zarezerwuj() {}
  '''void''' używaj() '''throws''' IOException {}
  '''void''' innaNiebezpiecznaOperacja() '''throws''' InnyMożliwyWyjątek {}
  '''void''' zwolnij() '''throws''' IOEception {}
}
'''public''' '''class''' ZwalnianieZasobów3 {
  '''public''' '''static''' '''void''' main(String[] args) '''throws''' Exception {
    Zasób3 z = null;
    '''try''' {
      z = '''new''' Zasób1(); //podczas inicjalizacji też mogą powstać wyjątki
      z.zarezerwuj();
      //tu jest niebezpieczny kod
      z.używaj();
      z.innaNiebezpiecznaOperacja();
      //...
    } '''catch''' (IOException e) {
      //obsługa wyjątku IOException
    } '''catch''' (InnyMożliwyWyjątek w) {
      //obsługa wyjątku InnyMożliwyWyjątek
    } '''finally''' {
        if (z != null) {
          try {
              zasób.zwolnij();
            } catch (IOException ex) {
              ex.printStackTrace();
            }
        }
    }
  }
}
A co jeżeli musimy naraz operować kilkoma zasobami? Wtedy kod w '''finally''' jeszcze bardziej się rozrośnie. Nie możemy przecież pozwolić żeby wyjątek zgłoszony przy zamykaniu pierwszego zasobu przeszkodził w zamykaniu pozostałych. Z tego powodu od Javy 7 rozbudowano jeszcze bardziej instrukcję '''try'''. Możemy zadeklarować z jakich zasobów będziemy korzystać, a kodu zwalniającego zasoby nie musimy już sami pisać. Wymagamy jedynie, żeby zasoby implementowały interfejs ''AuroCloseable'', który gwarantuje istnienie metody ''close()''. Kod inicjujący zasoby podaje się między nawiasami okrągłymi po słowie kluczowym '''try'''. Nowego słowa kluczowego nie wprowadzono ze względu na kompatybilność wsteczną. W przeciwnym przypadku wszystkie programy używające takiego słowa jako identyfikatora przestałyby być poprawne w nowej wersji Javy.
'''public''' '''class''' ZwalnianieZasobów4 {
  '''public''' '''static''' '''void''' main(String[] args) '''throws''' Exception {
    //Zasób1, Zasób2 i Zasób3 muszą implementować interfejs AutoCloseable
    //wymuszający istnienie metody close()
    '''try''' (Zasób1 z1 = '''new''' Zasób1();
                Zasób2 z2 = '''new''' Zasób2(0);
                Zasób3 z3 = '''new''' Zasób3()) {
      //tu jest niebezpieczny kod korzystający z z1, z2 i z3
      //...
    } '''catch''' (IOException e) {
      //obsługa wyjątku IOException
    } '''catch''' (InnyMożliwyWyjątek w) {
      //obsługa wyjątku InnyMożliwyWyjątek
    }
  }
}
=== Wielokrotna klauzula catch ===
W Javie 7 wprowadzono jeszcze jedno ułatwienie. Załóżmy, że w poprzednim przykładzie ''IOException'' jak i ''InnyMożliwyWyjątek'' są obsługiwane w ten sam sposób przy pomocy tego samego kodu. Leniwy programista mógłby próbować uprościć sobie życie łącząc te klauzule.
'''public''' '''class''' ZwalnianieZasobów5 {
  '''public''' '''static''' '''void''' main(String[] args) '''throws''' Exception {
    //Zasób1, Zasób2 i Zasób3 muszą implementować interfejs AutoCloseable
    //wymuszający istnienie metody close()
    '''try''' (Zasób1 z1 = '''new''' Zasób1();
                Zasób2 z2 = '''new''' Zasób2(0);
                Zasób3 z3 = '''new''' Zasób3()) {
      //tu jest niebezpieczny kod korzystający z z1, z2 i z3
      //...
    } '''catch''' (Exception e) {
      //obsługa wyjątku IOException oraz InnyMożliwyWyjątek
      //niestety lenistwo nie popłaca, teraz obsługujemy jeszcze wszystkie inne wyjątki,
      //na które nasz kod pewnie nie był przygotowany i które powinny wydostać się na zewnątrz
    }
  }
}
Niestety ''Exception'' jest nadklasą wszystkich wyjątków, również tych niekontrolowanych, a nie tylko ''IOException'' i ''InnyMożliwyWyjątek''. Takie uproszczenie zmienia działanie programu. Żeby temu zaradzić trzeba by sprawdzić typ przechwyconego wyjątku przy pomocy operatora '''instanceof''' i nieobsługiwane wyjątki zgłosić ponownie lub skorzystać z nowej możliwości - wielokrotnej klauzuli '''catch'''.
'''public''' '''class''' ZwalnianieZasobów6 {
  '''public''' '''static''' '''void''' main(String[] args) '''throws''' Exception {
    //Zasób1, Zasób2 i Zasób3 muszą implementować interfejs AutoCloseable
    //wymuszający istnienie metody close()
    '''try''' (Zasób1 z1 = '''new''' Zasób1();
                Zasób2 z2 = '''new''' Zasób2(0);
                Zasób3 z3 = '''new''' Zasób3()) {
      //tu jest niebezpieczny kod korzystający z z1, z2 i z3
      //...
    } '''catch''' (IOException | InnyMożliwyWyjątek e) {
      //obsługa wyjątku IOException oraz InnyMożliwyWyjątek
     }
     }
   }
   }
Linia 184: Linia 274:


== Wyjątki, a tworzenie i inicjalizacja obiektów ==
== Wyjątki, a tworzenie i inicjalizacja obiektów ==
Tworzenie i inicjalizacja obiektów następuje w chwili użycia słowa kluczowego '''new''' i wskazaniu po nim konstruktora. Najpierw, jeżeli jeszcze to nie było zrobione, maszyna wirtualna wczytuje definicję klasy i wykonuje jej inicjalizację statyczną. Podczas inicjalizacji statycznej nie mogą być zgłaszane żadne wyjątki. Dotyczy to zarówno inicjalizacji statycznych atrybutów jak i wykonywania kodu z bloków inicjalizacji statycznej ('''static { ... }'''). Gdy inicjalizacja statyczna się zakończy, bądź nie jest konieczna, bo maszyna wirtualna używała już tej klasy, następuje inicjalizacja egzemplarza i wykonywany jest wskazany konstruktor. Każdy wyjątek, który może być zgłoszony w trakcie inicjalizacji atrybutów egzemplarza lub podczas wykonywania kodu z bloków inicjalizacji ('''{ ... }''') musi być wymieniony w klauzulach '''throws''' wszystkich konstruktorów. Dodatkowo każdy konstruktor może jeszcze zgłaszać dalsze wyjątki.
Tworzenie i inicjalizacja obiektów następuje w chwili użycia słowa kluczowego '''new''' i wskazaniu po nim konstruktora. Najpierw, jeżeli jeszcze to nie było zrobione, maszyna wirtualna wczytuje definicję klasy i wykonuje jej inicjalizację statyczną. Podczas inicjalizacji statycznej nie mogą być zgłaszane żadne wyjątki kontrolowane. Dotyczy to zarówno inicjalizacji statycznych atrybutów jak i wykonywania kodu z bloków inicjalizacji statycznej ('''static { ... }'''). Gdy inicjalizacja statyczna się zakończy, bądź nie jest konieczna, bo maszyna wirtualna używała już tej klasy, następuje inicjalizacja egzemplarza i wykonywany jest wskazany konstruktor. Każdy wyjątek kontrolowany, który może być zgłoszony w trakcie inicjalizacji atrybutów egzemplarza lub podczas wykonywania kodu z bloków inicjalizacji ('''{ ... }'''), musi być wymieniony w klauzulach '''throws''' wszystkich konstruktorów. Dodatkowo każdy konstruktor może jeszcze zgłaszać dalsze wyjątki.
  '''class''' WyjA '''extends''' Exception {};
  '''class''' WyjA '''extends''' Exception {};
  '''class''' WyjB '''extends''' Exception {};
  '''class''' WyjB '''extends''' Exception {};
Linia 194: Linia 284:
   '''static''' '''void''' możeZgłosićWyjątek() '''throws''' WyjA {}
   '''static''' '''void''' możeZgłosićWyjątek() '''throws''' WyjA {}
   
   
   //wyjątki z bloków inicjalizacji muszą być wymienione w klauzulach throws wszystkich konstruktorów
   //kontrolowane wyjątki z bloków inicjalizacji muszą być wymienione w klauzulach throws wszystkich konstruktorów
   {
   {
     możeZgłosićWyjątek();
     możeZgłosićWyjątek();
   }
   }
    
    
   //podczas inicjalizacji składowych statycznych klasy nie można zgłaszać wyjątków
   //podczas inicjalizacji statycznej klasy nie można zgłaszać wyjątków kontrolowanych
   // static {  
   // static {  
   //  możeZgłosićWyjątek();
   //  możeZgłosićWyjątek();
   // }
   // }
    
    
   '''static''' int statyczna() '''throws''' WyjB {
   '''static''' '''int''' statyczna() '''throws''' WyjB {
     '''return''' 1;
     '''return''' 1;
   }
   }
    
    
   int normalna() '''throws''' WyjC {
   '''int''' normalna() '''throws''' WyjC {
     '''return''' 2;
     '''return''' 2;
   }
   }
    
    
   //wyjątki, które mogą być zgłoszone podczas inicjalizacji atrybutów egzemplarza
   //wyjątki kontrolowane, które mogą być zgłoszone podczas inicjalizacji atrybutów egzemplarza
   //muszą być wymienione w klauzulach throws wszystkich konstruktorów
   //muszą być wymienione w klauzulach throws wszystkich konstruktorów
   int i = normalna();
   '''int''' i = normalna();
   int j = statyczna();
   '''int''' j = statyczna();
    
    
   //podczas inicjalizacji składowych statycznych klasy nie można zgłaszać wyjątków
   //podczas inicjalizacji składowych statycznych klasy nie można zgłaszać wyjątków kontrolowanych
   // static k = statyczna();
   // static k = statyczna();
    
    
Linia 223: Linia 313:
   }
   }
   
   
   WyjątkiAInicjalizacja(int i) '''throws''' WyjA, WyjB, WyjC, WyjD {
   WyjątkiAInicjalizacja('''int''' i) '''throws''' WyjA, WyjB, WyjC, WyjD {
     '''throw''' '''new''' WyjD();
     '''throw''' '''new''' WyjD();
   }
   }
Linia 232: Linia 322:
  }
  }


== Wyjątki, a rozszerzanie obiektów ==
== Wyjątki a rozszerzanie obiektów ==
To jakie wyjątki mogą zgłaszać metody nadklasy ma wpływ na to jakie wyjątki mogą zgłaszać metody podklasy. Warto rozważyć trzy przypadki: przedefiniowane metody, nowe metody i konstruktory.
To, jakie kontrolowane wyjątki mogą zgłaszać metody nadklasy, ma wpływ na to, jakie kontrolowane wyjątki mogą zgłaszać metody podklasy. Warto rozważyć trzy przypadki: przedefiniowane metody, nowe metody i konstruktory.


==== Przedefiniowane metody ====
==== Przedefiniowane metody ====
Obiekty podklasy muszą się dawać używać w miejsce obiektów nadklasy. W przypadku metod oznacza to, że metody podklasy muszą być nadzbiorem metod z nadklasy. Tak samo jest w przypadku atrybutów. W przypadku wyjątków zgłaszanych przez każdą z metod jest dokładnie odwrotnie. Te zgłaszane przez (bardziej szczegółowe) metody z podklasy muszą być podzbiorem tych zgłaszanych przez (bardziej ogólne) metody z nadklasy. Jest tak dlatego, że żadna uszczegółowiona wersja metody nie może zgłaszać wyjątków, na które nie byłby przygotowany kod zakładający użycie ogólnej metody z nadklasy. Jeżeli w nadklasie metoda ''m()'' zdeklaruje, że zgłasza wyjątki ''A'' i ''B'', to jej uszczegółowiona wersja w podklasie może zgłaszać wyjątki ''A'' i ''B'' lub sam wyjątek ''A'' lub sam wyjątek ''B'' lub może nie zgłaszać żadnych wyjątków lub dowolne wyjątki, które są podklasami ''A'' albo ''B''. W poniższym przykładzie metody ''jedź()'' obu rodzai samochodu deklarują jedynie wyjątki uszczegółowione wersje wyjątku deklarowanego przez swoją nadklasę.
Obiekty podklasy muszą się dawać używać w miejsce obiektów nadklasy. W przypadku metod oznacza to, że metody podklasy muszą być nadzbiorem metod z nadklasy. W przypadku wyjątków kontrolowanych zgłaszanych przez każdą z metod jest dokładnie odwrotnie. Te zgłaszane przez (bardziej szczegółowe) metody z podklasy muszą być podzbiorem tych zgłaszanych przez (bardziej ogólne) metody z nadklasy. Jest tak dlatego, że żadna uszczegółowiona wersja metody nie może zgłaszać wyjątków, na które nie byłby przygotowany kod zakładający użycie ogólnej metody z nadklasy. Jeżeli w nadklasie metoda ''m()'' zdeklaruje, że zgłasza wyjątki kontrolowane ''A'' i ''B'', to jej uszczegółowiona wersja w podklasie może zgłaszać wyjątki ''A'' i ''B'' lub sam wyjątek ''A'' lub sam wyjątek ''B'' lub może nie zgłaszać żadnych wyjątków, lub dowolne wyjątki, które są podklasami ''A'' albo ''B''. W poniższym przykładzie metody ''jedź()'' z samochodów obu typów deklarują jedynie uszczegółowione wersje wyjątku deklarowanego przez swoją nadklasę.
  '''class''' BrakPaliwa '''extends''' Exception {}
  '''class''' BrakPaliwa '''extends''' Exception {}
  '''class''' BrakBenzyny '''extends''' BrakPaliwa {}
  '''class''' BrakBenzyny '''extends''' BrakPaliwa {}
Linia 252: Linia 342:
   '''void''' jedź() '''throws''' BrakBenzyny, BrakGazu {}
   '''void''' jedź() '''throws''' BrakBenzyny, BrakGazu {}
  }
  }
Te same ograniczenia co przy przesłanianiu metod dotyczą implementowania metod wymienionych w interfejsach. Jeżeli, któraś metoda wymieniana jest naraz w kilku implementowanych interfejsach i/lub nadklasie, deklarowane przez nią wyjątki muszą należeć do przecięcia zbiorów z poszczególnych deklaracji. Często to przecięcie będzie zbiorem pustym. W poniższym przykładzie metoda ''jedź()'' w klasie ''PerpetuumMobile'' nie może deklarować żadnych wyjątków, bo ''jedź()'' z klasy ''Samochód'' i ''jeźdź()'' z interfejsu ''WehikułCzasu'' nie deklarują wspólnych wyjątków.
Te same ograniczenia, co przy przesłanianiu metod, dotyczą implementowania metod wymienionych w interfejsach. Jeżeli któraś metoda wymieniana jest naraz w kilku implementowanych interfejsach i/lub nadklasie, deklarowane przez nią wyjątki muszą należeć do przecięcia zbiorów z poszczególnych deklaracji. Często to przecięcie będzie zbiorem pustym. W poniższym przykładzie metoda ''jedź()'' w klasie ''PerpetuumMobile'' nie może deklarować żadnych wyjątków, bo ''jedź()'' z klasy ''Samochód'' i ''jeźdź()'' z interfejsu ''WehikułCzasu'' nie deklarują wspólnych wyjątków.
  '''class''' BłądContinuum '''extends''' Exception {}
  '''class''' BłądContinuum '''extends''' Exception {}
  '''class''' ŁamiePrawaFizyki '''extends''' Exception {}
  '''class''' ŁamiePrawaFizyki '''extends''' Exception {}
Linia 262: Linia 352:
   
   
  '''class''' PerpetuumMobile '''extends''' Samochód '''implements''' WehikułCzasu {
  '''class''' PerpetuumMobile '''extends''' Samochód '''implements''' WehikułCzasu {
   '''public''' '''void''' jedź() {} //jedź() z Samochód i WehikułCzasu nie mają wspólnych wyjątków
   '''public''' '''void''' jedź() {} //nie może deklarować wyjątków, bo
                                    //jedź() z Samochód i z WehikułCzasu nie mają wspólnych wyjątków
   '''public''' '''void''' przenieśSięWCzasie() '''throws''' BłądContinuum {}  
   '''public''' '''void''' przenieśSięWCzasie() '''throws''' BłądContinuum {}  
  }
  }


==== Nowe metody ====
==== Nowe metody ====
Nowych (nie występujących w nadklasie) metod podklasy nie dotyczą żadne ograniczenia. Mogą zgłaszać dowolne wyjątki lub nie zgłaszać ich wcale.
Nowych (nie występujących w nadklasie) metod podklasy nie dotyczą żadne ograniczenia. Mogą zgłaszać dowolne wyjątki kontrolowane lub nie zgłaszać ich wcale.


==== Konstruktory ====
==== Konstruktory ====
Przypomnijmy, że podczas tworzenia obiektu najpierw odbywa się pełna inicjalizacja części ogólnej, w tym wywoływany jest któryś jej konstruktor. Jeżeli na przykład klasa ''B'' rozszerza klasę ''A'', to w chwili tworzenia nowego egzemplarza ''B'' najpierw następuje pełna inicjalizacja jego części odziedziczonej po klasie ''A''. Ta inicjalizacja kończy się wywoływaniem konstruktora z klasy ''A'', który wskazano na początku używanego konstruktora klasy ''B'' (jeżeli nie wskazano żadnego to używany jest bezparametrowy). Dopiero po zakończeniu konstruktora z części ogólnej inicjalizowana jest część szczegółowa &ndash; najpierw atrybuty i bloki inicjalizacji, a dopiero na końcu konstruktor. Konstruktor nadklasy nie jest więc wykonywany na początku konstruktora podklasy, ale tylko wskazuje się tam którego konstruktora użyć. To tylko taka notacja. Nie można tego konstruktora otoczyć instrukcją '''try-catch''' żeby obsłużyć wyjątki, które wystąpiły podczas inicjalizacji części ogólnej (pierwszą instrukcją byłoby wtedy '''try-catch'''). Jeśli się nad tym zastanowić obsługiwanie tych wyjątków nie ma sensu skoro nie da się powtórnie wymusić inicjalizacji części ogólnej. Wobec tego konstruktory podklasy muszą co najmniej deklarować wszystkie wyjątki, które deklaruje wskazywany przez nie konstruktor nadklasy. Nie ma natomiast żadnego powodu żeby nie miały deklarować więcej wyjątków. W poniższym przykładzie wszystkie konstruktory podklasy ''SamochódTerenowyONapędzieHybrydowym'' muszą deklarować wyjątek ''BrakGotówki'', bo jest on deklarowany przez jedyny konstruktor nadklasy ''SamochódTerenowy''.
Przypomnijmy, że podczas tworzenia obiektu najpierw odbywa się pełna inicjalizacja części ogólnej, w tym wywoływany jest któryś jej konstruktor. Jeżeli na przykład klasa ''B'' rozszerza klasę ''A'', to w chwili tworzenia nowego egzemplarza ''B'' najpierw następuje pełna inicjalizacja jego części odziedziczonej po klasie ''A''. Ta inicjalizacja kończy się wywoływaniem konstruktora z klasy ''A''. Który konstruktor nadklasy ''A'' użyć oraz jakie parametry przekazać, wskazuje się przy pomocy odwołania do '''super''' na początku konstruktora podklasy (jeżeli nie wskazano żadnego, to używany jest bezparametrowy). Dopiero po zakończeniu konstruktora z części ogólnej (''A'') inicjalizowana jest część szczegółowa (''B'') &ndash; najpierw atrybuty i bloki inicjalizacji, a dopiero na końcu konstruktor. Konstruktor nadklasy nie jest więc wykonywany na początku konstruktora podklasy, ale tylko wskazuje się tam, którego konstruktora użyć i jaki parametr mu przekazać. To tylko taka notacja. Nie można tego konstruktora otoczyć instrukcją '''try-catch''' żeby obsłużyć wyjątki, które wystąpiły podczas inicjalizacji części ogólnej (pierwszą instrukcją byłoby wtedy '''try-catch''', a nie odwołanie do '''super'''). Jeśli się nad tym zastanowić, obsługiwanie tych wyjątków nie ma sensu, skoro nie da się powtórzyć inicjalizacji części ogólnej. Wobec tego każdy błąd przy inicjalizacji części ogólnej uniemożliwia utworzenie egzemplarza i konstruktory podklasy muszą co najmniej deklarować wszystkie wyjątki, które deklaruje wskazywany przez nie konstruktor nadklasy. Nie ma natomiast żadnego powodu, żeby nie miały deklarować więcej wyjątków. W poniższym przykładzie wszystkie konstruktory podklasy ''SamochódTerenowyONapędzieHybrydowym'' muszą deklarować wyjątek ''BrakGotówki'', bo jest on deklarowany przez jedyny konstruktor nadklasy ''SamochódTerenowy''.
  '''class''' BrakGotówki '''extends''' Exception {}
  '''class''' BrakGotówki '''extends''' Exception {}
  '''class''' NiedostępnyWSprzedaży '''extends''' Exception {}
  '''class''' NiedostępnyWSprzedaży '''extends''' Exception {}
Linia 279: Linia 370:
  }
  }
   
   
  '''class''' SamochódTerenowyONapędzieHybrydowym '''extends''' Samochód {
  '''class''' SamochódTerenowyONapędzieHybrydowym '''extends''' SamochódTerenowy {
   SamochódTerenowyONapędzieHybrydowym() '''throws''' BrakGotówki, NiedostępnyWSprzedaży {
   SamochódTerenowyONapędzieHybrydowym() '''throws''' BrakGotówki, NiedostępnyWSprzedaży {
     //jeżeli nie wskazujemy konstruktora nadklasy, zostanie użyty bezparametrowy
     //jeżeli nie wskazujemy konstruktora nadklasy, zostanie użyty bezparametrowy
Linia 291: Linia 382:
   '''void''' jedź() '''throws''' BrakBenzyny, BrakGazu {}
   '''void''' jedź() '''throws''' BrakBenzyny, BrakGazu {}
  }
  }
== Dobre praktyki ==
=== Przy tworzeniu biblioteki ===
* Wyjątki kontrolowane stosuj, jeżeli jesteś pewien, że programista bezpośrednio korzystający z twoich bibliotek będzie mógł na nie właściwie zareagować. Jeżeli nie jesteś pewien używaj wyjątków niekontrolowanych.
* Nie obchodź kapsułkowania. Nie przepuszczaj wyjątków związanych z aktualną implementacją do wyższych warstw.
* Nie twórz nowych typów wyjątków, jeżeli nie przenoszą ważnych informacji.
* Umieszczaj informacje o możliwych wyjątkach w dokumentacji technicznej.
=== Podczas obsługi wyjątków ===
* Pamiętaj o zwalnianiu zasobów, używaj w tym celu klauzuli '''finally'''.
* Nie organizuj przepływu sterowania przy pomocy wyjątków. Twój kod będzie trudniejszy do zrozumienia oraz mniej efektywny, gdyż za każdym razem gdy zgłaszany jest wyjątek, maszyna wirtualna musi przygotować informacje o rozwijanym stosie wywołań.
* Jeżeli nie wiesz co zrobić z wyjątkiem kontrolowanym, zamień go na wyjątek niekontrolowany, ale w żadnym wypadku nie obsługuj przy pomocy pustego bloku '''catch'''.
* Pamiętaj, że przechwytując wyjątki dziedziczące po ''Exception'', przechwytujesz zarówno wyjątki kontrolowane jak niekontrolowane.


== Podsumowanie ==
== Podsumowanie ==
Mechanizm obsługi wyjątków pozwala przerwać normalny przepływ starowania w programie i przekazać sterowanie do miejsca, gdzie jest możliwe zareagowanie na sytuację wyjątkową. Wyjątki zostały wprowadzone do języków programowania, ponieważ obsługa wszystkich sytuacji wyjątkowych mogących wystąpić w trakcie wywoływania każdej kolejnej metody jest zbyt uciążliwa. Programy, w których po każdym wywołaniu metody trzeba sprawdzać jaki był efekt, są trudne do zrozumienia i mniej efektywne. Takie sposób obsługi sytuacji wyjątkowych skutkuje tym, że programiści unikają ich zgłaszania i obsługi. Wyjątki pozwalają ograniczyć ilość kodu koniecznego do obsługi sytuacji wyjątkowych dzięki obsłudze błędów bloku instrukcji. Umożliwiają również przeniesienie kodu obsługi z dala od miejsca wystąpienia wyjątku. Jeżeli nie wiadomo jak zareagować na dany wyjątek, najlepiej go po prostu w tym miejscu nie łapać. Dzięki zebraniu i przeniesieniu w dogodne miejsce kodu radzącego sobie z sytuacjami wyjątkowymi reszta programu staje się bardziej przejrzysta i spójna.
Wyjątki zostały wprowadzone do języków programowania, ponieważ obsługa wszystkich sytuacji wyjątkowych mogących wystąpić w trakcie wywoływania każdej kolejnej metody jest zbyt uciążliwa. Programy, w których po każdym wywołaniu metody trzeba było sprawdzać, jaki był efekt, są trudne do zrozumienia i mniej efektywne. Takie podejście do obsługi sytuacji wyjątkowych zniechęcało programistów do ich wykrywania i korygowania. Co więcej, dzięki obsłudze błędów z całego bloku instrukcji, ilość kodu koniecznego do obsługi sytuacji wyjątkowych ulega ograniczeniu.
 
Mechanizm obsługi wyjątków pozwala przenieść kod obsługi sytuacji wyjątkowej z dala od miejsca jej wykrycia. Zgłoszenie wyjątku przerywa normalny przepływ sterowania i rozpoczyna rozwijanie stosu wywołań, aż do napotkania kodu obsługującego ten typ wyjątku. Dzięki temu, jeżeli w danym miejscu nie wiadomo jak zareagować na dany wyjątek, po prostu się go ignoruje i obsługuje gdzie indziej. Zebranie i przeniesienie w dogodne miejsce kodu radzącego sobie z sytuacjami wyjątkowymi sprawia, że reszta programu staje się bardziej przejrzysta i spójna.


W Javie kompilator pilnuje, aby programista był świadom, wyjątków które mogą się pojawić. Jeżeli nie chce ich obsługiwać, powinny być wymienione w klauzuli '''throws'''. Wymienienie wyjątku w klauzuli '''throws''' może oznaczać dwie rzeczy: że gdzieś indziej należy się zająć danym wyjątkiem, bo tu jest zgłaszany albo że gdzieś indziej należy się zająć tym wyjątkiem, bo tu nie wiadomo co z nim zrobić. W obu przypadkach jest to świadoma decyzja. Żadne wyjątki nie zostaną przeoczone. Takie podejście ma swoje zalety. Nie jest to jednak standard. Niektórzy uważają, że specyfikowanie wyjątków, które mogą być zgłoszone wygląda tak zachęcająco, bo używane przykłady są bardzo proste. Podczas rozwijania i wprowadzania zmian do dużych systemów konieczność specyfikowania wyjątków przysporzyć wielu problemów. Co gorsza wiele osób zapomina o drugiej możliwości i tylko dlatego żeby zadowolić kompilator wyłapuje, a następnie ignoruje wyjątki, z którymi w danym kontekście nie wiadomo co zrobić. Jest to przykład na to, że żaden mechanizm językowy nie zmusi programistów do pisania dobrych programów, a jedynie może to ułatwić. W innych obiektowych językach programowania jak C# nie wymienia się zgłaszanych wyjątków. Nie zależnie od tego, które podejście uważasz za lepsze, pamiętaj że w Javie jest wybór. Kompilator nie wymusza specyfikowania wyjątków ''RuntimeException''. Wyjątki, których nie chcesz dopisywać w klauzulach '''throws''' może dołączyć jako przyczynę do nowego egzemplarza ''RuntimeException''. Możesz też rozszerzyć tą klasę i zupełnie zrezygnować z pomocy kompilatora.
W Javie są dwa rodzaje wyjątków: wyjątki kontrolowane i wyjątki niekontrolowane. Wyjątkich kontrolowane są bardzo przydatne w niektórych sytuacjach, np. podczas używania zasobów, które trzeba jawnie zwolnić, ale nie powinny być nadużywane.

Aktualna wersja na dzień 00:05, 19 mar 2012

<<< Powrót do przedmiotu Programowanie obiektowe

Zwalnianie zasobów

W Javie programista nie musi martwić się o rezerwowanie i zwalnianie pamięci. Odpowiada za to odśmiecacz. 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 możliwość wystąpienia wyjątku - jak nie trudno sobie wyobrazić - wcale nie ułatwia tego zadania. Niestety, 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ć. Pomiędzy tymi czynnościami wykonywane są niebezpieczne operacje. Wobec tego w każdym bloku 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 zadbać o zwolnienie naszego zasobu zanim je ponownie zgłosimy.

import java.io.IOException;

class InnyMożliwyWyjątek extends Exception {}

class Zasób1 {
  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ób1 z = new Zasób1();
    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
      //złapią się tu również wyjątki niekontrolowane (rozszerzające RuntimeException)

      //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. Trzeba pamiętać o wyjątkach niekontrolowanych oraz o wszystkich możliwościach opuszczenia bloku try. Zwolnienie zasobów może być np. konieczne przed każdym 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. Te przypadki pokazuje poniższy przykład.

import java.io.IOException;

class InnyMożliwyWyjątek extends Exception {}

class Zasób2 {
  Zasób2(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ób2 z = new Zasób2(i);
      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
        //złapią się tu również wyjątki niekontrolowane (rozszerzające RuntimeException)

        //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 się tym odśmiecacz, który wprawdzie wywołuje metodę finalize() każdego zwalnianego obiektu (można by próbować zwalniać w niej zasoby), ale nie ma gwarancji, że obiekty zostaną poddane procesowi odśmiecania. Może się przecież zdarzyć, że przez cały czas działania programu pamięci nie brakowało. Java oferuje inny mechanizm rekompensujący brak destruktora.

Blok finally

Do instrukcji try można dodać blok finally. Zawarty w nim 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. Blok finally jest doskonałym miejscem na zwalnianie wszelkiego rodzaju zasobów. Umieszcza się go bezpośrednio po ostatnim bloku 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
  //tu zwalniamy zasoby
}

Można też użyć samego bloku 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
  //tu zwalniamy zasoby
}

Jeżeli w bloku try nie wystąpił wyjątek, kod z bloku finally wykonywany jest bezpośrednio po jego zakończeniu. Jeżeli wyjątek wystąpił, ale nie pasuje do żadnego bloku catch lub nie umieszczono żadnego bloku catch, kod z bloku 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ący blok catch, to kod z bloku 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 tym bloku 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();
    }
  }
}

Try z zasobami

Klauzula finally ułatwia nam bezpieczne gospodarowanie zasobami, ale nadal zrobienie tego poprawnie jest pracochłonne i wymaga skupienia uwagi. W kolejnym przykładzie dodatkowo dopuścimy powstawanie wyjątków podczas inicjalizacji obiektu reprezentującego zasób oraz podczas jego zwalniania.

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

public class ZwalnianieZasobów3 {
  public static void main(String[] args) throws Exception {
    Zasób3 z = null;
    try {
      z = new Zasób1(); //podczas inicjalizacji też mogą powstać wyjątki
      z.zarezerwuj();

      //tu jest niebezpieczny kod
      z.używaj();
      z.innaNiebezpiecznaOperacja();
      //...
    } catch (IOException e) {
      //obsługa wyjątku IOException
    } catch (InnyMożliwyWyjątek w) {
      //obsługa wyjątku InnyMożliwyWyjątek
    } finally {
       if (z != null) {
          try {
             zasób.zwolnij();
           } catch (IOException ex) {
              ex.printStackTrace();
           }
        }
    }
  }
}

A co jeżeli musimy naraz operować kilkoma zasobami? Wtedy kod w finally jeszcze bardziej się rozrośnie. Nie możemy przecież pozwolić żeby wyjątek zgłoszony przy zamykaniu pierwszego zasobu przeszkodził w zamykaniu pozostałych. Z tego powodu od Javy 7 rozbudowano jeszcze bardziej instrukcję try. Możemy zadeklarować z jakich zasobów będziemy korzystać, a kodu zwalniającego zasoby nie musimy już sami pisać. Wymagamy jedynie, żeby zasoby implementowały interfejs AuroCloseable, który gwarantuje istnienie metody close(). Kod inicjujący zasoby podaje się między nawiasami okrągłymi po słowie kluczowym try. Nowego słowa kluczowego nie wprowadzono ze względu na kompatybilność wsteczną. W przeciwnym przypadku wszystkie programy używające takiego słowa jako identyfikatora przestałyby być poprawne w nowej wersji Javy.

public class ZwalnianieZasobów4 {
  public static void main(String[] args) throws Exception {
    //Zasób1, Zasób2 i Zasób3 muszą implementować interfejs AutoCloseable
    //wymuszający istnienie metody close()
    try (Zasób1 z1 = new Zasób1();
               Zasób2 z2 = new Zasób2(0);
               Zasób3 z3 = new Zasób3()) {
      //tu jest niebezpieczny kod korzystający z z1, z2 i z3
      //...
    } catch (IOException e) {
      //obsługa wyjątku IOException
    } catch (InnyMożliwyWyjątek w) {
      //obsługa wyjątku InnyMożliwyWyjątek
    }
  }
}

Wielokrotna klauzula catch

W Javie 7 wprowadzono jeszcze jedno ułatwienie. Załóżmy, że w poprzednim przykładzie IOException jak i InnyMożliwyWyjątek są obsługiwane w ten sam sposób przy pomocy tego samego kodu. Leniwy programista mógłby próbować uprościć sobie życie łącząc te klauzule.

public class ZwalnianieZasobów5 {
  public static void main(String[] args) throws Exception {
    //Zasób1, Zasób2 i Zasób3 muszą implementować interfejs AutoCloseable
    //wymuszający istnienie metody close()
    try (Zasób1 z1 = new Zasób1();
               Zasób2 z2 = new Zasób2(0);
               Zasób3 z3 = new Zasób3()) {
      //tu jest niebezpieczny kod korzystający z z1, z2 i z3
      //...
    } catch (Exception e) {
      //obsługa wyjątku IOException oraz InnyMożliwyWyjątek
      //niestety lenistwo nie popłaca, teraz obsługujemy jeszcze wszystkie inne wyjątki,
      //na które nasz kod pewnie nie był przygotowany i które powinny wydostać się na zewnątrz
    }
  }
}

Niestety Exception jest nadklasą wszystkich wyjątków, również tych niekontrolowanych, a nie tylko IOException i InnyMożliwyWyjątek. Takie uproszczenie zmienia działanie programu. Żeby temu zaradzić trzeba by sprawdzić typ przechwyconego wyjątku przy pomocy operatora instanceof i nieobsługiwane wyjątki zgłosić ponownie lub skorzystać z nowej możliwości - wielokrotnej klauzuli catch.

public class ZwalnianieZasobów6 {
  public static void main(String[] args) throws Exception {
    //Zasób1, Zasób2 i Zasób3 muszą implementować interfejs AutoCloseable
    //wymuszający istnienie metody close()
    try (Zasób1 z1 = new Zasób1();
               Zasób2 z2 = new Zasób2(0);
               Zasób3 z3 = new Zasób3()) {
      //tu jest niebezpieczny kod korzystający z z1, z2 i z3
      //...
    } catch (IOException | InnyMożliwyWyjątek e) {
      //obsługa wyjątku IOException oraz InnyMożliwyWyjątek
    }
  }
}

Wyjątki, a tworzenie i inicjalizacja obiektów

Tworzenie i inicjalizacja obiektów następuje w chwili użycia słowa kluczowego new i wskazaniu po nim konstruktora. Najpierw, jeżeli jeszcze to nie było zrobione, maszyna wirtualna wczytuje definicję klasy i wykonuje jej inicjalizację statyczną. Podczas inicjalizacji statycznej nie mogą być zgłaszane żadne wyjątki kontrolowane. Dotyczy to zarówno inicjalizacji statycznych atrybutów jak i wykonywania kodu z bloków inicjalizacji statycznej (static { ... }). Gdy inicjalizacja statyczna się zakończy, bądź nie jest konieczna, bo maszyna wirtualna używała już tej klasy, następuje inicjalizacja egzemplarza i wykonywany jest wskazany konstruktor. Każdy wyjątek kontrolowany, który może być zgłoszony w trakcie inicjalizacji atrybutów egzemplarza lub podczas wykonywania kodu z bloków inicjalizacji ({ ... }), musi być wymieniony w klauzulach throws wszystkich konstruktorów. Dodatkowo każdy konstruktor może jeszcze zgłaszać dalsze wyjątki.

class WyjA extends Exception {};
class WyjB extends Exception {};
class WyjC extends Exception {};
class WyjD extends Exception {};
class WyjE extends Exception {};

public class WyjątkiAInicjalizacja {
  static void możeZgłosićWyjątek() throws WyjA {}

  //kontrolowane wyjątki z bloków inicjalizacji muszą być wymienione w klauzulach throws wszystkich konstruktorów
  {
    możeZgłosićWyjątek();
  }
 
  //podczas inicjalizacji statycznej klasy nie można zgłaszać wyjątków kontrolowanych
  // static { 
  //   możeZgłosićWyjątek();
  // }
 
  static int statyczna() throws WyjB {
    return 1;
  }
 
  int normalna() throws WyjC {
    return 2;
  }
 
  //wyjątki kontrolowane, które mogą być zgłoszone podczas inicjalizacji atrybutów egzemplarza
  //muszą być wymienione w klauzulach throws wszystkich konstruktorów
  int i = normalna();
  int j = statyczna();
 
  //podczas inicjalizacji składowych statycznych klasy nie można zgłaszać wyjątków kontrolowanych
  // static k = statyczna();
 
  WyjątkiAInicjalizacja() throws WyjA, WyjB, WyjC {
  }

  WyjątkiAInicjalizacja(int i) throws WyjA, WyjB, WyjC, WyjD {
    throw new WyjD();
  }

  WyjątkiAInicjalizacja(String s) throws WyjA, WyjB, WyjC, WyjE {
    throw new WyjE();
  }
}

Wyjątki a rozszerzanie obiektów

To, jakie kontrolowane wyjątki mogą zgłaszać metody nadklasy, ma wpływ na to, jakie kontrolowane wyjątki mogą zgłaszać metody podklasy. Warto rozważyć trzy przypadki: przedefiniowane metody, nowe metody i konstruktory.

Przedefiniowane metody

Obiekty podklasy muszą się dawać używać w miejsce obiektów nadklasy. W przypadku metod oznacza to, że metody podklasy muszą być nadzbiorem metod z nadklasy. W przypadku wyjątków kontrolowanych zgłaszanych przez każdą z metod jest dokładnie odwrotnie. Te zgłaszane przez (bardziej szczegółowe) metody z podklasy muszą być podzbiorem tych zgłaszanych przez (bardziej ogólne) metody z nadklasy. Jest tak dlatego, że żadna uszczegółowiona wersja metody nie może zgłaszać wyjątków, na które nie byłby przygotowany kod zakładający użycie ogólnej metody z nadklasy. Jeżeli w nadklasie metoda m() zdeklaruje, że zgłasza wyjątki kontrolowane A i B, to jej uszczegółowiona wersja w podklasie może zgłaszać wyjątki A i B lub sam wyjątek A lub sam wyjątek B lub może nie zgłaszać żadnych wyjątków, lub dowolne wyjątki, które są podklasami A albo B. W poniższym przykładzie metody jedź() z samochodów obu typów deklarują jedynie uszczegółowione wersje wyjątku deklarowanego przez swoją nadklasę.

class BrakPaliwa extends Exception {}
class BrakBenzyny extends BrakPaliwa {}
class BrakGazu extends BrakPaliwa {}

abstract class Samochód {
  abstract void jedź() throws BrakPaliwa;
}

class SamochódNaBenzynę extends Samochód {
  void jedź() throws BrakBenzyny {}
}

class SamochódONapędzieHybrydowym extends Samochód {
  void jedź() throws BrakBenzyny, BrakGazu {}
}

Te same ograniczenia, co przy przesłanianiu metod, dotyczą implementowania metod wymienionych w interfejsach. Jeżeli któraś metoda wymieniana jest naraz w kilku implementowanych interfejsach i/lub nadklasie, deklarowane przez nią wyjątki muszą należeć do przecięcia zbiorów z poszczególnych deklaracji. Często to przecięcie będzie zbiorem pustym. W poniższym przykładzie metoda jedź() w klasie PerpetuumMobile nie może deklarować żadnych wyjątków, bo jedź() z klasy Samochód i jeźdź() z interfejsu WehikułCzasu nie deklarują wspólnych wyjątków.

class BłądContinuum extends Exception {}
class ŁamiePrawaFizyki extends Exception {}

interface WehikułCzasu {
  void jedź() throws ŁamiePrawaFizyki;
  void przenieśSięWCzasie() throws BłądContinuum;
}

class PerpetuumMobile extends Samochód implements WehikułCzasu {
  public void jedź() {} //nie może deklarować wyjątków, bo
                                    //jedź() z Samochód i z WehikułCzasu nie mają wspólnych wyjątków
  public void przenieśSięWCzasie() throws BłądContinuum {} 
}

Nowe metody

Nowych (nie występujących w nadklasie) metod podklasy nie dotyczą żadne ograniczenia. Mogą zgłaszać dowolne wyjątki kontrolowane lub nie zgłaszać ich wcale.

Konstruktory

Przypomnijmy, że podczas tworzenia obiektu najpierw odbywa się pełna inicjalizacja części ogólnej, w tym wywoływany jest któryś jej konstruktor. Jeżeli na przykład klasa B rozszerza klasę A, to w chwili tworzenia nowego egzemplarza B najpierw następuje pełna inicjalizacja jego części odziedziczonej po klasie A. Ta inicjalizacja kończy się wywoływaniem konstruktora z klasy A. Który konstruktor nadklasy A użyć oraz jakie parametry przekazać, wskazuje się przy pomocy odwołania do super na początku konstruktora podklasy (jeżeli nie wskazano żadnego, to używany jest bezparametrowy). Dopiero po zakończeniu konstruktora z części ogólnej (A) inicjalizowana jest część szczegółowa (B) – najpierw atrybuty i bloki inicjalizacji, a dopiero na końcu konstruktor. Konstruktor nadklasy nie jest więc wykonywany na początku konstruktora podklasy, ale tylko wskazuje się tam, którego konstruktora użyć i jaki parametr mu przekazać. To tylko taka notacja. Nie można tego konstruktora otoczyć instrukcją try-catch żeby obsłużyć wyjątki, które wystąpiły podczas inicjalizacji części ogólnej (pierwszą instrukcją byłoby wtedy try-catch, a nie odwołanie do super). Jeśli się nad tym zastanowić, obsługiwanie tych wyjątków nie ma sensu, skoro nie da się powtórzyć inicjalizacji części ogólnej. Wobec tego każdy błąd przy inicjalizacji części ogólnej uniemożliwia utworzenie egzemplarza i konstruktory podklasy muszą co najmniej deklarować wszystkie wyjątki, które deklaruje wskazywany przez nie konstruktor nadklasy. Nie ma natomiast żadnego powodu, żeby nie miały deklarować więcej wyjątków. W poniższym przykładzie wszystkie konstruktory podklasy SamochódTerenowyONapędzieHybrydowym muszą deklarować wyjątek BrakGotówki, bo jest on deklarowany przez jedyny konstruktor nadklasy SamochódTerenowy.

class BrakGotówki extends Exception {}
class NiedostępnyWSprzedaży extends Exception {}

class SamochódTerenowy extends Samochód {
  SamochódTerenowy() throws BrakGotówki {}
  void jedź() throws BrakPaliwa {}
}

class SamochódTerenowyONapędzieHybrydowym extends SamochódTerenowy {
  SamochódTerenowyONapędzieHybrydowym() throws BrakGotówki, NiedostępnyWSprzedaży {
    //jeżeli nie wskazujemy konstruktora nadklasy, zostanie użyty bezparametrowy
  }
  SamochódTerenowyONapędzieHybrydowym(String s) throws BrakGotówki, NiedostępnyWSprzedaży {
    //tu tylko wskazujemy, który konstruktor nadklasy wybrać
    //zostanie on wykonany wcześniej, więc nie możemy go otoczyć instrukcją try-catch
    super();
    throw new NiedostępnyWSprzedaży();
  }
  void jedź() throws BrakBenzyny, BrakGazu {}
}

Dobre praktyki

Przy tworzeniu biblioteki

  • Wyjątki kontrolowane stosuj, jeżeli jesteś pewien, że programista bezpośrednio korzystający z twoich bibliotek będzie mógł na nie właściwie zareagować. Jeżeli nie jesteś pewien używaj wyjątków niekontrolowanych.
  • Nie obchodź kapsułkowania. Nie przepuszczaj wyjątków związanych z aktualną implementacją do wyższych warstw.
  • Nie twórz nowych typów wyjątków, jeżeli nie przenoszą ważnych informacji.
  • Umieszczaj informacje o możliwych wyjątkach w dokumentacji technicznej.

Podczas obsługi wyjątków

  • Pamiętaj o zwalnianiu zasobów, używaj w tym celu klauzuli finally.
  • Nie organizuj przepływu sterowania przy pomocy wyjątków. Twój kod będzie trudniejszy do zrozumienia oraz mniej efektywny, gdyż za każdym razem gdy zgłaszany jest wyjątek, maszyna wirtualna musi przygotować informacje o rozwijanym stosie wywołań.
  • Jeżeli nie wiesz co zrobić z wyjątkiem kontrolowanym, zamień go na wyjątek niekontrolowany, ale w żadnym wypadku nie obsługuj przy pomocy pustego bloku catch.
  • Pamiętaj, że przechwytując wyjątki dziedziczące po Exception, przechwytujesz zarówno wyjątki kontrolowane jak niekontrolowane.

Podsumowanie

Wyjątki zostały wprowadzone do języków programowania, ponieważ obsługa wszystkich sytuacji wyjątkowych mogących wystąpić w trakcie wywoływania każdej kolejnej metody jest zbyt uciążliwa. Programy, w których po każdym wywołaniu metody trzeba było sprawdzać, jaki był efekt, są trudne do zrozumienia i mniej efektywne. Takie podejście do obsługi sytuacji wyjątkowych zniechęcało programistów do ich wykrywania i korygowania. Co więcej, dzięki obsłudze błędów z całego bloku instrukcji, ilość kodu koniecznego do obsługi sytuacji wyjątkowych ulega ograniczeniu.

Mechanizm obsługi wyjątków pozwala przenieść kod obsługi sytuacji wyjątkowej z dala od miejsca jej wykrycia. Zgłoszenie wyjątku przerywa normalny przepływ sterowania i rozpoczyna rozwijanie stosu wywołań, aż do napotkania kodu obsługującego ten typ wyjątku. Dzięki temu, jeżeli w danym miejscu nie wiadomo jak zareagować na dany wyjątek, po prostu się go ignoruje i obsługuje gdzie indziej. Zebranie i przeniesienie w dogodne miejsce kodu radzącego sobie z sytuacjami wyjątkowymi sprawia, że reszta programu staje się bardziej przejrzysta i spójna.

W Javie są dwa rodzaje wyjątków: wyjątki kontrolowane i wyjątki niekontrolowane. Wyjątkich kontrolowane są bardzo przydatne w niektórych sytuacjach, np. podczas używania zasobów, które trzeba jawnie zwolnić, ale nie powinny być nadużywane.