Kiedy należy zgłosić wyjątek IllegalArgumentException?

98

Martwię się, że jest to wyjątek w czasie wykonywania, więc prawdopodobnie powinno być używane oszczędnie.
Standardowy przypadek użycia:

void setPercentage(int pct) {
    if( pct < 0 || pct > 100) {
         throw new IllegalArgumentException("bad percent");
     }
}

Ale wydaje się, że wymusiłoby to następujący projekt:

public void computeScore() throws MyPackageException {
      try {
          setPercentage(userInputPercent);
      }
      catch(IllegalArgumentException exc){
           throw new MyPackageException(exc);
      }
 }

Aby przywrócić go jako sprawdzony wyjątek.

Dobra, ale chodźmy z tym. Jeśli podasz złe dane wejściowe, pojawi się błąd wykonania. Po pierwsze, jest to dość trudna zasada do jednolitego wdrożenia, ponieważ może być konieczne wykonanie odwrotnej konwersji:

public void scanEmail(String emailStr, InputStream mime) {
    try {
        EmailAddress parsedAddress = EmailUtil.parse(emailStr);
    }
    catch(ParseException exc){
        throw new IllegalArgumentException("bad email", exc);
    }
}

Co gorsza - o ile 0 <= pct && pct <= 100można oczekiwać statycznego sprawdzenia kodu klienta, nie dotyczy to bardziej zaawansowanych danych, takich jak adres e-mail, lub, co gorsza, czegoś, co należy sprawdzić w bazie danych, dlatego generalnie kod klienta nie może wstępnie uprawomocnić.

Zasadniczo mówię, że nie widzę sensownej, spójnej polityki dotyczącej używania IllegalArgumentException. Wydaje się, że nie należy go używać i powinniśmy trzymać się własnych sprawdzonych wyjątków. Jaki jest dobry przypadek użycia, aby to wyrzucić?

djechlin
źródło

Odpowiedzi:

80

Dokument API dla IllegalArgumentException:

Zgłoszony, aby wskazać, że do metody został przekazany niedozwolony lub niewłaściwy argument.

Patrząc na to, jak jest używany w bibliotekach JDK , powiedziałbym:

  • Wydaje się, że środkiem obronnym jest narzekanie na oczywiście złe dane wejściowe, zanim dane wejściowe dostaną się do pracy i spowodują awarię w połowie z bezsensownym komunikatem o błędzie.

  • Jest używany w przypadkach, w których rzucenie sprawdzonego wyjątku byłoby zbyt irytujące (chociaż pojawia się w kodzie java.lang.reflect, gdzie obawa o absurdalne poziomy rzucania sprawdzanych wyjątków nie jest widoczna).

Użyłbym IllegalArgumentExceptiondo ostatniego defensywnego sprawdzania argumentów dla popularnych narzędzi (starając się zachować spójność z użyciem JDK). Lub gdy oczekuje się, że zły argument jest błędem programisty, podobnie jak plik NullPointerException. Nie użyłbym go do implementacji walidacji w kodzie biznesowym. Z pewnością nie użyłbym tego w przykładzie e-mailowym.

Nathan Hughes
źródło
8
Myślę, że rada „gdzie oczekuje się, że zły argument jest błędem programisty” jest najbardziej zgodna z tym, jak widziałem to użycie, więc akceptuję tę odpowiedź.
djechlin
22

Mówiąc o „złym wejściu”, należy zastanowić się, skąd pochodzi dane wejściowe.

Jeśli dane wejściowe zostały wprowadzone przez użytkownika lub inny system zewnętrzny, nad którym nie masz kontroli, należy oczekiwać, że dane wejściowe będą nieprawidłowe i zawsze je sprawdzać. W tym przypadku można zgłosić wyjątek zaznaczony. Twoja aplikacja powinna „odzyskać” po tym wyjątku, przekazując użytkownikowi komunikat o błędzie.

Jeśli dane wejściowe pochodzą z twojego własnego systemu, np. Bazy danych lub innych części twojej aplikacji, powinieneś być w stanie polegać na tym, że jest poprawny (powinien zostać zweryfikowany zanim tam dotarł). W tym przypadku jest całkowicie w porządku, aby zgłosić niezaznaczony wyjątek, taki jak IllegalArgumentException, którego nie należy przechwytywać (generalnie nigdy nie należy łapać niezaznaczonych wyjątków). To błąd programisty, że w pierwszej kolejności trafiła tam nieprawidłowa wartość;) Musisz to naprawić.

Tomek
źródło
2
Dlaczego „nigdy nie powinieneś łapać niezaznaczonych wyjątków”?
Koray Tugay
9
Ponieważ niesprawdzony wyjątek ma zostać zgłoszony w wyniku błędu programowania. Nie można racjonalnie oczekiwać, że obiekt wywołujący metodę, która zgłasza takie wyjątki, odzyska z niej korzyści, dlatego zazwyczaj nie ma sensu ich przechwytywać.
Tom
1
Because an unchecked exception is meant to be thrown as a result of a programming errorpomogło mi wyczyścić wiele rzeczy z mojej głowy, dziękuję :)
svarog
14

Rzucanie wyjątków w czasie wykonywania „oszczędnie” nie jest naprawdę dobrą polityką - efektywna Java zaleca używanie sprawdzonych wyjątków, gdy można racjonalnie oczekiwać, że wywołujący odzyska . (Błąd programisty jest konkretnym przykładem: jeśli konkretny przypadek wskazuje na błąd programisty, powinieneś zgłosić niesprawdzony wyjątek; chcesz, aby programista miał ślad stosu, gdzie wystąpił problem logiczny, a nie próbować samodzielnie go obsłużyć).

Jeśli nie ma nadziei na wyzdrowienie, możesz skorzystać z niezaznaczonych wyjątków; nie ma sensu ich łapać, więc to jest w porządku.

Z twojego przykładu nie wynika jednak w 100%, który przypadek znajduje się w twoim kodzie.

Louis Wasserman
źródło
Myślę, że „rozsądne oczekiwanie wyzdrowienia” jest bezsensowne. foo(data)Mogłaby wystąpić dowolna operacja , w ramach for(Data data : list) foo(data);której wywołujący mógłby chcieć, aby jak najwięcej zakończyło się sukcesem, nawet jeśli niektóre dane są źle sformułowane. Obejmuje również błędy programistyczne, jeśli awaria mojej aplikacji oznacza, że ​​transakcja nie przejdzie, to prawdopodobnie lepiej, jeśli oznacza to, że chłodzenie jądrowe przechodzi w tryb offline, to źle.
djechlin
StackOverflowErrori są to przypadki, po których nie można racjonalnie oczekiwać, że dzwoniący wyzdrowieje. Ale wygląda na to, że należy sprawdzić każdą wielkość liter na poziomie logiki danych lub aplikacji. Oznacza to, że sprawdzaj swój pusty wskaźnik!
djechlin
4
W zastosowaniach do chłodzenia jądrowego wolałbym ciężko oblać testy, niż pozwolić, aby przypadek, który programista uważał za niemożliwy do przejścia niezauważony.
Louis Wasserman,
Boolean.parseBoolean (..) zgłasza wyjątek IllegalArugmentException, mimo że „można racjonalnie oczekiwać, że wywołujący odzyska”. więc ..... to zależy od twojego kodu, aby go obsłużyć lub zawrócić do dzwoniącego.
Jeryl Cook
5

Jak określono w oficjalnym samouczku Oracle, stwierdza się, że:

Jeśli można racjonalnie oczekiwać, że klient odzyska sprawność po wyjątku, uczyń go sprawdzonym wyjątkiem. Jeśli klient nie może nic zrobić, aby odzyskać od wyjątku, uczyń go niezaznaczonym wyjątkiem.

Jeśli mam aplikację korzystającą z bazy danych przy użyciu JDBCi mam metodę, która przyjmuje argument jako int itemi double price. priceDo odpowiedniego elementu jest odczytywany z tabeli bazy danych. Po prostu mnożę całkowitą liczbę itemzakupów przez pricewartość i zwracam wynik. Chociaż zawsze jestem pewien na końcu (koniec aplikacji), że wartość pola ceny w tabeli nigdy nie może być ujemna, ale co, jeśli wartość ceny wyjdzie ujemna ? Pokazuje, że po stronie bazy danych jest poważny problem. Być może błędne wprowadzenie ceny przez operatora. Jest to rodzaj problemu, którego druga część aplikacji wywołująca tę metodę nie może przewidzieć i nie może jej naprawić. To jest BUGw twojej bazie danych. Tak iIllegalArguementException()należy wyrzucić w tym przypadku, co by to oznaczało the price can't be negative.
Mam nadzieję, że jasno wyraziłem swój punkt widzenia.

Vishal K.
źródło
Nie podoba mi się ta rada (wyroczni), ponieważ obsługa wyjątków dotyczy tego, jak odzyskać, a nie czy odzyskać. Na przykład jedno źle sformułowane żądanie użytkownika nie jest warte awarii całego serwera WWW.
djechlin
5

Każde API powinno sprawdzić poprawność każdego parametru dowolnej metody publicznej przed jej wykonaniem:

void setPercentage(int pct, AnObject object) {
    if( pct < 0 || pct > 100) {
        throw new IllegalArgumentException("pct has an invalid value");
    }
    if (object == null) {
        throw new IllegalArgumentException("object is null");
    }
}

Stanowią 99,9% przypadków błędów w aplikacji, ponieważ prosi ona o niemożliwe operacje, więc ostatecznie są to błędy, które powinny spowodować awarię aplikacji (więc jest to błąd nieodwracalny).

W takim przypadku i zgodnie z podejściem polegającym na szybkim niepowodzeniu, należy pozwolić aplikacji zakończyć się, aby uniknąć uszkodzenia stanu aplikacji.

Ignacio Soler Garcia
źródło
Wręcz przeciwnie, jeśli klient API daje mi złe wejście, powinienem nie upaść cały mój serwer API.
djechlin
2
Oczywiście nie powinno to spowodować awarii serwera API, ale zwrócić wyjątek do wywołującego, który nie powinien powodować awarii niczego poza klientem.
Ignacio Soler Garcia
To, co napisałeś w komentarzu, nie jest tym, co napisałeś w odpowiedzi.
djechlin
1
Pozwólcie mi wyjaśnić, że jeśli wywołanie API z niewłaściwymi parametrami (błąd) jest wykonywane przez klienta zewnętrznego, klient powinien się zawiesić. Jeśli to serwer API ma błąd wywołujący metodę ze złymi parametrami, to serwer API powinien ulec awarii. Sprawdź: en.wikipedia.org/wiki/Fail-fast
Ignacio Soler Garcia
2

Potraktuj IllegalArgumentExceptionjako sprawdzenie warunków wstępnych i rozważ zasadę projektowania: metoda publiczna powinna zarówno znać, jak i publicznie dokumentować swoje własne warunki wstępne.

Zgadzam się, że ten przykład jest poprawny:

void setPercentage(int pct) {
    if( pct < 0 || pct > 100) {
         throw new IllegalArgumentException("bad percent");
     }
}

Jeśli EmailUtil jest nieprzezroczysty , co oznacza, że ​​z jakiegoś powodu warunki wstępne nie mogą być opisane użytkownikowi końcowemu, to zaznaczony wyjątek jest poprawny. Druga wersja, poprawiona dla tego projektu:

import com.someoneelse.EmailUtil;

public void scanEmail(String emailStr, InputStream mime) throws ParseException {
    EmailAddress parsedAddress = EmailUtil.parseAddress(emailStr);
}

Jeśli EmailUtil jest przezroczysty , na przykład może jest to prywatna metoda należąca do danej klasy, IllegalArgumentExceptionjest poprawna wtedy i tylko wtedy, gdy jej warunki wstępne można opisać w dokumentacji funkcji. To jest również poprawna wersja:

/** @param String email An email with an address in the form [email protected]
 * with no nested comments, periods or other nonsense.
 */
public String scanEmail(String email)
  if (!addressIsProperlyFormatted(email)) {
      throw new IllegalArgumentException("invalid address");
  }
  return parseEmail(emailAddr);
}
private String parseEmail(String emailS) {
  // Assumes email is valid
  boolean parsesJustFine = true;
  // Parse logic
  if (!parsesJustFine) {
    // As a private method it is an internal error if address is improperly
    // formatted. This is an internal error to the class implementation.
    throw new AssertError("Internal error");
  }
}

Ten projekt może iść w obie strony.

  • Jeśli opisanie warunków wstępnych jest drogie lub jeśli klasa ma być używana przez klientów, którzy nie wiedzą, czy ich e-maile są prawidłowe, użyj ParseException. Nazwa metody najwyższego poziomu scanEmailwskazuje, że użytkownik końcowy zamierza wysłać niezbadaną wiadomość e-mail, więc prawdopodobnie jest to poprawne.
  • Jeśli warunki wstępne można opisać w dokumentacji funkcji, a klasa nie ma zamiaru nieprawidłowych danych wejściowych i dlatego wskazywany jest błąd programisty, użyj IllegalArgumentException. Chociaż nie jest „zaznaczone”, „czek” przenosi się do Javadoc dokumentującego funkcję, do której klient powinien się stosować. IllegalArgumentExceptiongdy klient nie może wcześniej stwierdzić, że jego argument jest nielegalny, jest błędny.

Uwaga dotycząca wyjątku IllegalStateException : Oznacza to, że „stan wewnętrzny tego obiektu (zmienne instancji prywatnej) nie może wykonać tej czynności”. Użytkownik końcowy nie widzi stanu prywatnego tak luźno mówiąc, że ma on pierwszeństwo IllegalArgumentExceptionw przypadku, gdy wywołanie klienta nie ma możliwości sprawdzenia, czy stan obiektu jest niespójny. Nie mam dobrego wyjaśnienia, kiedy jest to preferowane od sprawdzonych wyjątków, chociaż przykładami są takie rzeczy, jak dwukrotna inicjalizacja lub utrata połączenia z bazą danych, które nie jest odzyskiwane.

djechlin
źródło