Obejście dla sprawdzonych wyjątków Java

49

Bardzo doceniam nowe funkcje Java 8 dotyczące lambd i interfejsów metod domyślnych. Nadal jednak nudzą mnie sprawdzone wyjątki. Na przykład, jeśli chcę tylko wymienić wszystkie widoczne pola obiektu, chciałbym po prostu napisać:

    Arrays.asList(p.getClass().getFields()).forEach(
        f -> System.out.println(f.get(p))
    );

Ponieważ jednak getmetoda może zgłosić sprawdzony wyjątek, co nie jest zgodne z Consumerumową interfejsu, muszę złapać ten wyjątek i napisać następujący kod:

    Arrays.asList(p.getClass().getFields()).forEach(
            f -> {
                try {
                    System.out.println(f.get(p));
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
    );

Jednak w większości przypadków chcę, aby wyjątek został zgłoszony jako a RuntimeExceptioni pozwolił programowi obsłużyć wyjątek bez błędów kompilacji.

Tak więc chciałbym poznać Twoją opinię na temat mojego kontrowersyjnego obejścia problemu sprawdzonych irytacji wyjątków. W tym celu stworzyłem interfejs pomocniczy ConsumerCheckException<T>i funkcję narzędzia rethrow( zaktualizowaną zgodnie z sugestią komentarza Dovala ) w następujący sposób:

  @FunctionalInterface
  public interface ConsumerCheckException<T>{
      void accept(T elem) throws Exception;
  }

  public class Wrappers {
      public static <T> Consumer<T> rethrow(ConsumerCheckException<T> c) {
        return elem -> {
          try {
            c.accept(elem);
          } catch (Exception ex) {
            /**
             * within sneakyThrow() we cast to the parameterized type T. 
             * In this case that type is RuntimeException. 
             * At runtime, however, the generic types have been erased, so 
             * that there is no T type anymore to cast to, so the cast
             * disappears.
             */
            Wrappers.<RuntimeException>sneakyThrow(ex);
          }
        };
      }

      /**
       * Reinier Zwitserloot who, as far as I know, had the first mention of this
       * technique in 2009 on the java posse mailing list.
       * http://www.mail-archive.com/[email protected]/msg05984.html
       */
      public static <T extends Throwable> T sneakyThrow(Throwable t) {
          throw (T) t;
      }
  }

A teraz mogę po prostu napisać:

    Arrays.asList(p.getClass().getFields()).forEach(
            rethrow(f -> System.out.println(f.get(p)))
    );

Nie jestem pewien, czy jest to najlepszy idiom do odwrócenia sprawdzonych wyjątków, ale jak wyjaśniłem, chciałbym mieć wygodniejszy sposób na osiągnięcie mojego pierwszego przykładu bez zajmowania się sprawdzonymi wyjątkami i jest to prostszy sposób, który znalazłem zrobić to.

Fernando Miguel Carvalho
źródło
2
Oprócz linku Roberta, spójrz także na Wyjątkowo sprawdzone wyjątki sprawdzania rzucania . Jeśli chcesz, możesz użyć sneakyThrowwewnątrz, rethrowaby rzucić oryginalny, sprawdzony wyjątek, zamiast owijać go w RuntimeException. Alternatywnie możesz użyć @SneakyThrowsadnotacji z Project Lombok, która robi to samo.
Doval
1
Zauważ również, że Consumers in forEachmoże być wykonywane równolegle, gdy używasz równoległych Streams. Rzut rzucony z odbiorcą zostanie następnie przeniesiony do wątku wywołującego, który 1) nie zatrzyma innych równolegle działających konsumentów, co może, ale nie musi być odpowiednie, oraz 2) jeśli więcej niż jeden konsument rzuci coś, tylko jeden z rzucanych przedmiotów będzie widoczny przez wzywający wątek.
Joonas Pulakka
2
Jagnięta są takie brzydkie.
Tulains Córdova,

Odpowiedzi:

16

Zalety, wady i ograniczenia twojej techniki:

  • Jeśli kod wywołujący ma obsługiwać zaznaczony wyjątek, MUSISZ dodać go do klauzuli throws metody zawierającej strumień. Kompilator nie zmusi Cię do dodania go, więc łatwiej go zapomnieć. Na przykład:

    public void test(Object p) throws IllegalAccessException {
        Arrays.asList(p.getClass().getFields()).forEach(rethrow(f -> System.out.println(f.get(p))));
    }
    
  • Jeśli kod wywołujący już obsługuje sprawdzony wyjątek, kompilator przypomni o dodaniu klauzuli throws do deklaracji metody zawierającej strumień (jeśli tego nie powiesz: wyjątek nigdy nie jest generowany w treści odpowiedniej instrukcji try) .

  • W każdym razie nie będziesz w stanie otoczyć samego strumienia, aby złapać sprawdzony wyjątek WEWNĄTRZ metody, która zawiera strumień (jeśli spróbujesz, kompilator powie: wyjątek nigdy nie jest generowany w treści odpowiedniej instrukcji try).

  • Jeśli wywołujesz metodę, która dosłownie nigdy nie może zgłosić wyjątku, który deklaruje, nie należy dołączać klauzuli throws. Na przykład: nowy ciąg (byteArr, „UTF-8”) zgłasza wyjątek UnsupportedEncodingException, ale UTF-8 gwarantuje, że specyfikacja Java będzie zawsze obecna. Tutaj deklaracja rzutów jest uciążliwa i mile widziane jest każde rozwiązanie, aby ją wyciszyć przy minimalnej płycie kotła.

  • Jeśli nie znosisz sprawdzonych wyjątków i uważasz, że nigdy nie powinny być dodawane do języka Java na początek (rosnąca liczba osób myśli w ten sposób, a ja NIE jestem jednym z nich), to po prostu nie dodawaj sprawdzonego wyjątku do rzutów klauzula metody zawierającej strumień. Zaznaczony wyjątek będzie zatem zachowywał się tak, jak wyjątek NIEZaznaczony.

  • Jeśli implementujesz ścisły interfejs, w którym nie masz możliwości dodania deklaracji wyrzutów, a mimo to zgłoszenie wyjątku jest całkowicie właściwe, wówczas zawinięcie wyjątku tylko w celu uzyskania przywileju rzucenia go powoduje utworzenie stosu z fałszywymi wyjątkami, które nie dostarczaj żadnych informacji o tym, co faktycznie poszło nie tak. Dobrym przykładem jest Runnable.run (), która nie zgłasza żadnych sprawdzonych wyjątków. W takim przypadku możesz zdecydować, aby nie dodawać zaznaczonego wyjątku do klauzuli throws metody zawierającej strumień.

  • W każdym razie, jeśli zdecydujesz się NIE dodawać (lub zapomnieć dodać) zaznaczony wyjątek do klauzuli throws metody zawierającej strumień, pamiętaj o tych 2 konsekwencjach zgłaszania wyjątków CHECKED:

    1. Kod wywołujący nie będzie w stanie przechwycić go po nazwie (jeśli spróbujesz, kompilator powie: wyjątek nigdy nie jest generowany w treści odpowiedniej instrukcji try). Bąbelkuje i prawdopodobnie zostanie złapany w głównej pętli programu przez jakiś „catch Exception” lub „catch Throwable”, co może być tym, czego chcesz.

    2. Narusza to zasadę najmniejszego zaskoczenia: nie będzie już wystarczające do przechwycenia wyjątku RuntimeException, aby zagwarantować złapanie wszystkich możliwych wyjątków. Z tego powodu uważam, że nie należy tego robić w kodzie frameworka, a jedynie w kodzie biznesowym, który całkowicie kontrolujesz.

Bibliografia:

UWAGA: Jeśli zdecydujesz się użyć tej techniki, możesz skopiować LambdaExceptionUtilklasę pomocnika ze StackOverflow: https://stackoverflow.com/questions/27644361/how-can-i-throw-checked-exceptions-from-inside-java-8 -streams . Daje pełną implementację (funkcja, konsument, dostawca ...) z przykładami.

MarcG
źródło
6

W tym przykładzie, czy to naprawdę może zawieść? Nie myśl tak, ale może twoja sprawa jest wyjątkowa. Jeśli to naprawdę „nie może zawieść”, a jest to po prostu irytujący kompilator, lubię zawrzeć wyjątek i wrzucić Errorkomentarz z komentarzem „nie może się zdarzyć”. Sprawia, że utrzymanie jest bardzo jasne . W przeciwnym razie będą się zastanawiać: „jak to się może zdarzyć” i „do kogo, do cholery, się tym zajmuje?

Jest to w kontrowersyjnej praktyce, więc YMMV. Prawdopodobnie dostanę trochę głosów negatywnych.

użytkownik949300
źródło
3
+1, ponieważ rzucisz błąd. Ile razy skończyłem po godzinach debugowania w bloku catch zawierającym tylko jeden wiersz: „nie może się zdarzyć” ...
Axel
3
-1, ponieważ rzucisz błąd. Błędy mają wskazywać, że coś jest nie tak w JVM, a wyższe poziomy mogą odpowiednio sobie z nimi poradzić. Jeśli wybierzesz ten sposób, powinieneś zgłosić wyjątek RuntimeException. Innymi możliwymi obejściami są asercje (wymagają włączenia flagi -ea) lub rejestrowania.
duros
3
@duros: W zależności od tego, dlaczego wyjątek „nie może się zdarzyć”, jego zgłoszenie może wskazywać, że coś jest bardzo nie tak. Załóżmy, na przykład, jeśli ktoś wywołuje clonezapieczętowany typ, o Fooktórym wiadomo, że go obsługuje, i rzuca CloneNotSupportedException. Jak to się mogło stać, gdyby kod nie został powiązany z jakimś innym nieoczekiwanym rodzajem Foo? A jeśli tak się stanie, czy można cokolwiek zaufać?
supercat
1
@supercat doskonały przykład. Inni zgłaszają wyjątki od brakujących kodowań łańcuchowych lub testów wiadomości Jeśli brakuje UTF-8 lub SHA, środowisko uruchomieniowe prawdopodobnie jest uszkodzone.
user949300
1
@duros To kompletne nieporozumienie. An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.(cytat z Javadoc z Error) To jest właśnie taka sytuacja, dlatego nie jest ona nieodpowiednia, rzucanie Errortutaj jest najbardziej odpowiednią opcją.
biziclop
1

inna wersja tego kodu, w której sprawdzany wyjątek jest tylko opóźniony:

public class Cocoon {
         static <T extends Throwable> T forgetThrowsClause(Throwable t) throws T{
            throw (T) t;
        }

        public static <X, T extends Throwable> Consumer<X> consumer(PeskyConsumer<X,T> touchyConsumer) throws T {
            return new Consumer<X>() {
                @Override
                public void accept(X t) {
                    try {
                        touchyConsumer.accept(t);
                    } catch (Throwable exc) {
                        Cocoon.<RuntimeException>forgetThrowsClause(exc) ;
                    }

                }
            } ;
        }
// and so on for Function, and other codes from java.util.function
}

magia polega na tym, że jeśli zadzwonisz:

 myArrayList.forEach(Cocoon.consumer(MyClass::methodThatThrowsException)) ;

wtedy twój kod będzie musiał złapać wyjątek

java bear
źródło
Co się stanie, jeśli zostanie uruchomiony w równoległym strumieniu, w którym elementy są konsumowane w różnych wątkach?
przyciąć
0

sneakyThrow jest fajny! Całkowicie czuję twój ból.

Jeśli musisz zostać w Javie ...

Paguro ma funkcjonalne interfejsy, które owijają sprawdzone wyjątki, więc nie musisz już o tym myśleć. Obejmuje niezmienne kolekcje i transformacje funkcjonalne na wzór przetworników Clojure (lub strumieni Java 8), które przyjmują interfejsy funkcjonalne i zawijają sprawdzone wyjątki.

Istnieje również VAVr i kolekcje Eclipse do wypróbowania.

W przeciwnym razie użyj Kotlina

Kotlin jest dwukierunkowo kompatybilny z Javą, nie ma sprawdzonych wyjątków, nie ma prymitywów (no cóż, programista nie musi o tym myśleć), lepszy system pisma, zakłada niezmienność itp. Mimo, że kuratoruję bibliotekę Paguro powyżej, ja ' Konwertowanie całego kodu Java, który mogę, na Kotlin. Cokolwiek robiłem wcześniej w Javie, teraz wolę robić w Kotlinie.

GlenPeterson
źródło
Ktoś głosował za tym, co jest w porządku, ale nic się nie nauczę, chyba że powiesz mi, dlaczego głosowałeś za nim.
GlenPeterson
1
+1 dla twojej biblioteki w github, możesz także sprawdzić eclipse.org/collections lub project vavr, które zapewniają funkcjonalne i niezmienne zbiory. Co o tym sądzisz?
firephil
-1

Sprawdzone wyjątki są bardzo przydatne, a ich obsługa zależy od rodzaju produktu, do którego kodujesz:

  • Biblioteka
  • aplikacja komputerowa
  • serwer działający w jakimś polu
  • ćwiczenie akademickie

Zwykle biblioteka nie może obsługiwać sprawdzonych wyjątków, ale zadeklarować jako zgłoszony przez publiczny interfejs API. Rzadkim przypadkiem, w którym można było owinąć je w zwykły RuntimeExcepton, jest na przykład utworzenie w kodzie biblioteki prywatnego dokumentu XML i parsowanie go, utworzenie pliku tymczasowego, a następnie odczytanie go.

W przypadku aplikacji komputerowych należy rozpocząć opracowywanie produktu, tworząc własną strukturę obsługi wyjątków. Używając własnego frameworka zwykle zawijasz sprawdzony wyjątek w niektórych dwóch lub czterech opakowaniach (twoich niestandardowych podklasach RuntimeExcepion) i obsługujesz je za pomocą jednego modułu obsługi wyjątków.

W przypadku serwerów zwykle kod będzie działał wewnątrz jakiegoś frameworka. Jeśli nie używasz żadnego (czystego serwera), stwórz własną strukturę podobną (na podstawie Runtimes) opisaną powyżej.

W przypadku ćwiczeń akademickich zawsze możesz owinąć je bezpośrednio w RuntimeExcepton. Ktoś może również stworzyć małe ramy.

Razmik
źródło
-1

Możesz rzucić okiem na ten projekt ; Stworzyłem go specjalnie z powodu problemu sprawdzonych wyjątków i interfejsów funkcjonalnych.

Dzięki niemu możesz to zrobić zamiast pisać własny kod:

// I don't know the type of f, so it's Foo...
final ThrowingConsumer<Foo> consumer = f -> System.out.println(f.get(p));

Ponieważ wszystkie interfejsy do rzucania * rozszerzają swoje odpowiedniki do rzucania, możesz ich używać bezpośrednio w strumieniu:

Arrays.asList(p.getClass().getFields()).forEach(consumer);

Lub możesz po prostu:

import static com.github.fge.lambdas.consumers.Consumers.wrap;

Arrays.asList(p.getClass().getFields())
    .forEach(wrap(f -> System.out.println(f.get(p)));

I inne rzeczy.

fge
źródło
Jeszcze lepiejfields.forEach((ThrowingConsumer<Field>) f -> out.println(f.get(o)))
user2418306,
Czy downvoters proszę wyjaśnić?
fge