PO Wyjątki c.d.

Z Studia Informatyczne
Wersja z dnia 01:17, 30 lip 2006 autorstwa Jsroka (dyskusja | edycje)
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)
Przejdź do nawigacjiPrzejdź do wyszukiwania

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.

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 {}
  //wyjątki z bloków inicjalizacji muszą być wymienione w klauzulach throws wszystkich konstruktorów
  {
    możeZgłosićWyjątek();
  }
 
  //podczas inicjalizacji składowych statycznych klasy nie można zgłaszać wyjątków
  // static { 
  //   możeZgłosićWyjątek();
  // }
 
  static int statyczna() throws WyjB {
    return 1;
  }
 
  int normalna() throws WyjC {
    return 2;
  }
 
  //wyjątki, 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
  // 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 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.

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ę.

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ź() {} //jedź() z Samochód i 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 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 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 – 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. 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.

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ód {
  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 {}
}

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 dogodnie miejsce kodu radzącego sobie z sytuacjami wyjątkowymi 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.