Myślę, że spotkałeś się już z sytuacją, w której uruchamiasz kod, w wyniku czego otrzymujesz coś takiego jak NullPointerException , ClassCastException lub gorzej. Potem długie debugowanie, parsowanie, googlowanie i tak dalej. Wyjątki same w sobie są świetną rzeczą: wskazują, gdzie wystąpił problem i jakiego rodzaju. Jeśli chcesz odświeżyć sobie pamięć i po prostu dowiedzieć się więcej, zajrzyj do artykułu Wyjątki: sprawdzone, niesprawdzone i własne (codegym.cc)

Ale mogą zaistnieć sytuacje, w których trzeba utworzyć własny wyjątek. Na przykład Twój kod musi zażądać informacji od usługi zdalnej i z jakiegoś powodu jest ona niedostępna. Lub osoba wypełnia wniosek o kartę bankową i wprowadza swój numer telefonu, a przypadkowo lub nie, wprowadza numer, który jest już w systemie i należy do innego użytkownika.

Tutaj oczywiście zależy to jeszcze od wymagań klienta i architektury systemu, ale załóżmy, że otrzymano zadanie sprawdzenia numeru telefonu, a jeśli jest już w użyciu, rzućmy wyjątek.

Tworzymy wyjątek:


public class PhoneNumberAlreadyExistsException extends Exception {

   public PhoneNumberAlreadyExistsException(String message) {
       super(message);
   }
}
    

Użyjmy go do weryfikacji:


public class PhoneNumberRegisterService {
   List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");

   public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
       if (registeredPhoneNumbers.contains(phoneNumber)) {
           throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
       }
   }
}
    

Aby uprościć zadanie, „zakodujemy na stałe” kilka numerów telefonów - niech to będzie nasza baza danych. I na koniec spróbujmy zastosować nasz wyjątek:


public class CreditCardIssue {
   public static void main(String[] args) {
       PhoneNumberRegisterService service = new PhoneNumberRegisterService();
       try {
           service.validatePhone("+1-111-111-11-14");
       } catch (PhoneNumberAlreadyExistsException e) {
           // Here we can write to logs or display the call stack
		e.printStackTrace();
       }
   }
}
    

Cóż, czas nacisnąć Shift + F10 (jeśli używasz IDEA), czyli uruchomić projekt. A oto, co zobaczysz w konsoli:

wyjątek.CreditCardIssue
wyjątek.PhoneNumberIsExistException: Podany numer telefonu jest już używany przez innego klienta!
w wyjątku.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)

Cóż, stworzyłeś swój własny wyjątek, a nawet trochę go przetestowałeś. Gratulujemy tego osiągnięcia! Polecam trochę poeksperymentować, aby lepiej zrozumieć, jak to działa.

Dodaj jeszcze jedną rzecz: na przykład, aby sprawdzić obecność liter. Jak zapewne wiesz, w Stanach Zjednoczonych często używa się liter, aby ułatwić zapamiętanie numeru, na przykład 1-800-MOJE-JABŁKO. Oznacza to, że musisz sprawdzić, czy liczba zawiera tylko liczby.

Stworzyliśmy więc sprawdzony, czyli sprawdzony wyjątek. I wszystko byłoby dobrze, ale…

Społeczność programistów dzieli się na dwa obozy – tych, którzy są za sprawdzonymi wyjątkami i tych, którzy są temu przeciwni. Obie strony przedstawiają mocne argumenty. Wśród nich i innych znajdują się programiści pozaklasowi: Bruce Eckel krytykuje koncepcję sprawdzonych wyjątków, broni James Gosling. Wygląda na to, że ten problem nigdy nie zostanie trwale zamknięty. Przyjrzyjmy się jednak głównym wadom używania sprawdzonych wyjątków.

Główną wadą sprawdzanych wyjątków jest to, że muszą być obsługiwane. I tutaj mamy dwie opcje: albo obsłużymy go na miejscu za pomocą try-catch , albo, jeśli mamy ten sam wyjątek używany w wielu miejscach, rzucimy go za pomocą rzutów powyżej i obsłużymy je w klasach najwyższego poziomu.

Możemy też mieć „arkusz” kodu lub, jak czasem można usłyszeć, szablon, czyli dużo kodu, który zajmuje dużo miejsca, ale niesie ze sobą niewielki ładunek semantyczny.

Problemy zaczynają się w dość dużych aplikacjach: jest obsługiwanych wiele wyjątków, a metoda górnej warstwy może łatwo rozrosnąć się do listy rzutów z tuzinem wyjątków.

public OurCoolClass() zgłasza wyjątek FirstException, SecondException, ThirdException, ApplicationNameException...

Często programistom się to nie podoba i wybierają pewną sztuczkę: dziedziczą wszystkie sprawdzone wyjątki od tego samego przodka — ApplicationNameException . Teraz muszą go również złapać w module obsługi ( sprawdzone !):


catch (FirstException e) {
    // TODO
}
catch (SecondException e) {
    // TODO
}
catch (ThirdException e) {
    // TODO
}
catch (ApplicationNameException e) {
    // TODO
}
    

Tutaj czeka nas kolejny problem: co zrobić w ostatnim haczyku ? Powyżej omówiliśmy już wszystkie standardowe sytuacje, które podaliśmy, ale tutaj ApplicationNameException oznacza dla nas tylko wyjątek : „jakiś rodzaj niezrozumiałego błędu”. Tak przetwarzamy:


catch (ApplicationNameException e) {
    LOGGER.error("Unknown error", e.getMessage());
}
    

I w końcu nie wiemy, co się stało.

Ale wydawać by się mogło, że wszystko da się rzucić jednym ruchem ręki:


public void ourCoolMethod() throws Exception {
// Do some work
}
    

Tak, możesz. Ale jakie informacje niesie ze sobą „zgłasza wyjątek”? Coś się zepsuło. Będziesz musiał sprawdzić wszystko wewnątrz i na zewnątrz, a z debuggerem zaprzyjaźnisz się przez długi czas, aby zrozumieć przyczynę.

Można też natknąć się na konstrukcję, którą czasem nazywa się „złapany - bądź cicho”:


try {
// Some code
} catch(Exception e) {
   throw new ApplicationNameException("Error");
}
    

Żadne dodatkowe słowa nie są tu potrzebne - wszystko jest jasne z kodu, a raczej nic nie jest jasne.

Oczywiście możesz argumentować, że nie zobaczysz tego w prawdziwym kodzie. Dobra, spójrzmy na rzecz zwaną klasą URL z pakietu java.net i zajrzyjmy w jej głębię, w jej kod. Obserwuj mnie, jeśli chcesz wiedzieć!

Oto jeden z konstruktorów adresów URL :


public URL(String spec) throws MalformedURLException {
   this(null, spec);
}
    

Jak widać, jest tutaj ciekawie sprawdzony MalformedURLException . I można to rzucić w przypadku, cytuję:
jeśli nie określono żadnego protokołu lub znaleziono nieznany protokół, lub spec jest pusty, lub przeanalizowany adres URL nie jest zgodny ze specyficzną składnią powiązanego protokołu.

To jest:

  1. Jeśli protokół nie jest określony.
  2. Znaleziono nieznany protokół.
  3. Specyfikacja jest zerowa .
  4. Adres URL nie pasuje do określonej składni powiązanego protokołu.

Stwórzmy metodę, która utworzy obiekt klasy URL :


public URL createURL() {
   URL url = new URL("https://codegym.cc");
   return url;
}
    

Gdy tylko napiszesz te linie w IDE (ja piszę w IDEA, ale nawet w Eclipse i NetBeans to działa), zobaczysz to:

To mówi nam, że musimy zgłosić wyjątek lub zawinąć go w try-catch . Proponuję na razie wybrać drugą opcję, aby wizualnie zobaczyć, co się stanie:


public static URL createURL() {
   URL url = null;
   try {
       url = new URL("https://codegym.cc");
   } catch(MalformedURLException e) {
  e.printStackTrace();
   }
   return url;
}
    

Jak widać, jest już dość gadatliwy. Właściwie zostało to już powiedziane powyżej. Jest to jeden z najbardziej oczywistych powodów używania niesprawdzonych wyjątków.

Możemy stworzyć taki wyjątek, rozszerzając RuntimeException w Javie.

Niesprawdzone wyjątki są dziedziczone z klasy Error lub klasy RuntimeException . Wielu programistów uważa, że ​​nie możemy obsłużyć tych wyjątków w naszych programach, ponieważ są one rodzajem błędu, którego nie można oczekiwać, aby został naprawiony po wykonaniu kodu podczas działania programu.

Kiedy pojawia się niesprawdzony wyjątek, zwykle jest to spowodowane niewłaściwym użyciem kodu, przekazaniem wartości null lub innym błędnym argumentem.

Napiszmy więc kod:


public class OurCoolUncheckedException extends RuntimeException {
   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }
  
   public OurCoolUncheckedException(String message, Throwable throwable) {
       super(message, throwable);
   }
}
    

Uwaga: stworzyliśmy kilka konstruktorów do różnych celów. To pozwala nam rozszerzyć zakres naszego wyjątku. Tutaj na przykład możemy to zrobić, aby wyjątek mógł dać nam kod błędu. Na początek utwórzmy enum , w którym w rzeczywistości będą znajdować się nasze kody błędów:


public enum ErrorCodes {
   FIRST_ERROR(1),
   SECOND_ERROR(2),
   THIRD_ERROR(3);

   private int code;

   ErrorCodes(int code) {
       this.code = code;
   }

   public int getCode() {
       return code;
   }
}
    

Teraz dodajmy kolejny konstruktor do naszej klasy wyjątków:


public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
   super(message, cause);
   this.errorCode = errorCode.getCode();
}
    

I tak, nie zapomnijmy dodać pola, prawie zapomnieliśmy:


private Integer errorCode;
    

I oczywiście metoda uzyskania tego kodu:


public Integer getErrorCode() {
   return errorCode;
}
    

Spójrzmy na całą klasę, abyśmy mogli sprawdzić i porównać:

public class OurCoolUncheckedException extends RuntimeException {
   private Integer errorCode;

   public OurCoolUncheckedException(String message) {
       super(message);
   }

   public OurCoolUncheckedException(Throwable cause) {
       super(cause);
   }

   public OurCoolUncheckedException(String message, Throwable throwable) {

       super(message, throwable);
   }

   public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
       super(message, cause);
       this.errorCode = errorCode.getCode();
   }
   public Integer getErrorCode() {
       return errorCode;
   }
}
    

Oto nasz wyjątek i gotowe! Jak widać, nie ma nic szczególnie skomplikowanego. Sprawdźmy to w działaniu:


   public static void main(String[] args) {
       getException();
   }
   public static void getException() {
       throw new OurCoolUncheckedException("Our cool exception!");
   }
    

Uruchommy naszą małą aplikację i zobaczmy coś takiego w konsoli:

Teraz skorzystajmy z dodatkowej funkcjonalności, którą dodaliśmy. Dodajmy trochę do poprzedniego kodu:


public static void main(String[] args) throws Exception {

   OurCoolUncheckedException exception = getException(3);
   System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
   throw exception;

}

public static OurCoolUncheckedException getException(int errorCode) {
   return switch (errorCode) {
   case 1:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
   case 2:
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
   default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
       return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}

}
    

Wyjątki mogą być obsługiwane jak obiekty, chociaż jestem pewien, że już wiesz, że wszystko w Javie jest obiektem.

I spójrz, co zrobiliśmy. Najpierw zmieniliśmy metodę, która teraz nie rzuca, a po prostu wyrzuca wyjątek, w zależności od tego, który parametr otrzymaliśmy. Następnie, używając switch-case, generujemy wyjątek z kodem błędu i komunikatem, którego potrzebujemy. A w metodzie main otrzymaliśmy utworzony wyjątek, otrzymaliśmy kod błędu i wyrzuciliśmy go.

Uruchommy i zobaczmy, co dostanie się do konsoli:

I spójrz, okazuje się, że wydrukowaliśmy kod błędu, który otrzymaliśmy z wyjątku, a następnie rzuciliśmy sam wyjątek. Jednocześnie możemy nawet śledzić, gdzie dokładnie został zgłoszony wyjątek. W razie potrzeby możesz dodać wszystkie niezbędne informacje do wiadomości, utworzyć niezbędne kody błędów, uzupełnić swoje wyjątki o nowe funkcje.

Jak? Mam nadzieję, że wszystko Ci się udało!

Generalnie temat wyjątków jest dość obszerny i mało jednoznaczny. Będzie o wiele więcej sporów i wiele kopii zostanie uszkodzonych. Na przykład tylko Java sprawdziła wyjątki. Spośród najpopularniejszych języków nie widziałem takiego, który by ich używał.

Bruce Eckel bardzo dobrze napisał o wyjątkach w swojej książce „Java Philosophy”, w rozdziale 12, polecam ją przeczytać! Zajrzyj również do pierwszego tomu „Java. Horstmann's Professional Library, rozdział 7 - jest tam też sporo ciekawych rzeczy.

Małe wyniki

  1. Zapisuj wszystko do dziennika! Rejestruj komunikaty, które może zgłosić wyjątek. W większości przypadków bardzo pomoże to w debugowaniu i pozwoli ci zrozumieć, co się stało. Nie pozostawiaj bloku catch pustego , w przeciwnym razie po prostu „zje” wyjątek i nie będziesz mieć żadnych danych do rozwiązania problemu.

  2. Zła praktyka z wyjątkami polega na łapaniu ich wszystkich naraz (jak powiedział jeden z moich kolegów, to nie Pokemon - to Java), więc unikaj catch(Exception e) lub gorzej, catch(Throwable t) .

  3. Zgłoś wyjątek tak szybko, jak to możliwe. To jest dobra praktyka programowania w Javie. Studiując frameworki takie jak Spring, zobaczysz, że działają one na zasadzie Failed First. Oznacza to, że „upadnij” tak wcześnie, jak to możliwe, aby szybko znaleźć błąd. Wiąże się to oczywiście z pewnymi niedogodnościami. Jednak takie podejście pomaga stworzyć bardziej niezawodny kod.

  4. Podczas wywoływania innych części kodu najlepiej jest wychwycić pewne wyjątki. Jeśli wywołany kod zgłasza wiele wyjątków, zła praktyka programistyczna polega na przechwytywaniu tylko klasy nadrzędnej tych wyjątków. Na przykład, jeśli wywołany kod zgłasza FileNotFoundException obejmujący IOException . A w twoim kodzie, który wywołuje ten moduł, lepiej napisać dwa bloki catch, aby przechwycić każdy z wyjątków, zamiast jednego bloku catch do przechwycenia Exception .

  5. Wyłapuj wyjątki tylko wtedy, gdy możesz je skutecznie obsłużyć dla użytkownika i debugowania.

  6. Nie krępuj się napisać swoje wyjątki. Oczywiście w Javie jest wiele gotowych, na każdy gust i kolor, ale czasami nadal trzeba stworzyć własny „rower”. Ale musisz jasno zrozumieć, dlaczego to robisz i mieć pewność, że wśród personelu nie ma nikogo, kogo potrzebujesz.

  7. Podczas tworzenia klas wyjątków pamiętaj o nazewnictwie! Zapewne już wiesz, że poprawne nazewnictwo klas, zmiennych, metod i pakietów jest niezwykle ważne. Wyjątki nie są wyjątkami (przepraszam za tautologię)! Zawsze umieszczaj słowo wyjątek na końcu , a nazwa wyjątku musi jasno określać błąd, który przechwytuje. Przykładem jest FileNotFoundException .

  8. Wyjątki od dokumentów. Wskazane jest, aby napisać @throws javadoc dla wyjątków. Będzie to szczególnie przydatne w przypadkach, gdy twoje projekty zapewniają jakiekolwiek interfejsy. Tak, a później łatwiej będzie zrozumieć własny kod. Jak myślisz, gdzie możesz dowiedzieć się, co robi MalformedURLException ? Z javadoca! Tak, perspektywa napisania dokumentacji nie jest zbyt zachęcająca, ale wierz mi, podziękujesz sobie, gdy pół roku później wrócisz do własnego kodu.

  9. Zwolnij zasoby i nie zaniedbuj konstrukcji try-with-resources . Jeśli nadal nie wiesz, czym one są, sprawdź je tutaj — Java 7 try-with-resources .

  10. To raczej podsumowanie: mądrze używaj wyjątków. Zgłaszanie wyjątku to operacja dość wymagająca zasobów. Możliwe, że w wielu przypadkach łatwiej będzie nie wyrzucać wyjątków, ale zwrócić, powiedzmy, zmienną boolowską, która wskazuje, jak przebiegła operacja, używając prostego i „tańszego” if-else .

    Kuszące może być również powiązanie logiki aplikacji z wyjątkami, co zdecydowanie nie jest tego warte. Wyjątki, jak powiedzieliśmy na początku artykułu, to sytuacja wyjątkowa, a nie regularna i istnieją różne narzędzia, aby im „zapobiegać”. W szczególności istnieje opcjonalne , aby zapobiec wyjątkowi NullPointerException lub Scanner.hasNext i tym podobne, aby zapobiec wyjątkowi IOException , który może rzucić metoda read() .