Jak mogę zgłosić WYJĄTKOWO wyjątki ze strumieni Java 8?

287

W jaki sposób mogę zgłaszać sprawdzone wyjątki z wewnętrznych strumieni Java 8 / lambdas?

Innymi słowy, chcę, aby kod taki jak ten był kompilowany:

public List<Class> getClasses() throws ClassNotFoundException {     

    List<Class> classes = 
        Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
              .map(className -> Class.forName(className))
              .collect(Collectors.toList());                  
    return classes;
    }

Ten kod nie kompiluje się, ponieważ Class.forName()powyższa metoda rzuca ClassNotFoundException, co jest sprawdzane.

Uwaga: NIE chcę zawijać zaznaczonego wyjątku w wyjątku środowiska wykonawczego i zamiast tego rzucać zawinięty, niesprawdzony wyjątek. Chcę zgłosić sam sprawdzony wyjątek i bez dodawania brzydkiego try/ catchesdo strumienia.

MarcG
źródło
42
Krótka odpowiedź brzmi: nie możesz, bez łamania zasad dotyczących wyjątków. Możesz oczywiście oszukiwać, a inni chętnie pokażą ci, jak to zrobić, ale pamiętaj, że to oszustwo i takie oszustwo często powraca, by cię ugryźć. Powinieneś złapać wyjątek i sobie z nim poradzić. Jeśli chcesz go owinąć, a następnie ponownie sprawdzić zaznaczony wyjątek, możesz to zrobić bezpiecznie.
Brian Goetz,
35
@Brian, nie potrzebuję, aby inni mówili mi, jak oszukiwać, wiem, jak sam siebie oszukiwać, i podałem swój sposób oszukiwania w odpowiedzi poniżej, którą głosowałeś. Wiem, że jesteś zaangażowany w dyskusję o Javie, która zdecydowała, że ​​nie ma dobrego sposobu radzenia sobie ze sprawdzonymi wyjątkami w strumieniach, więc wydaje mi się niesamowite, że zauważyłeś moje pytanie, ale jestem rozczarowany twoją odpowiedzią, która mówi „to jest nie jest dobre ”, nie podaje żadnego powodu, a potem znów dodaje try / catch.
MarcG,
22
@Brian, szczerze mówiąc, w praktyce, kiedy ludzie próbują refaktoryzować starsze stwierdzenia, połowa z nich jest konwertowana na strumienie, ale druga połowa rezygnuje z refaktoryzacji, ponieważ nikt nie chce dodawać tych prób / chwytów. Sprawiają, że kod jest znacznie trudniejszy do odczytania, z pewnością bardziej niż oryginalne instrukcje for. W powyższym przykładzie kodu, dopóki utrzymujesz „wyrzuca ClassNotFoundException”, nie widzę żadnej różnicy w stosunku do kodu zewnętrznego. Czy mógłbyś podać mi przykłady z życia, w których łamie to zasady dotyczące wyjątków?
MarcG,
10
Pisanie metod opakowujących, które zawijają się do niesprawdzonych wyjątków, rozwiązuje problem „zaśmiecania kodu” i nie psuje systemu typów. Odpowiedź, która ucieka się do „podstępnego rzutu” sprawdzonego wyjątku, psuje system typów, ponieważ kod wywołujący nie będzie oczekiwał (ani nie będzie mógł złapać) sprawdzonego wyjątku.
Brian Goetz,
14
Nie rozwiązuje problemu z zaśmieceniem kodu, ponieważ wtedy potrzebujesz drugiej próby / złapania strumienia, aby rozpakować i powtórzyć oryginalny wyjątek. Przeciwnie, jeśli zgłaszasz sprawdzony wyjątek, potrzebujesz tylko zachować throws ClassNotFoundExceptionmetodę w deklaracji metody zawierającej strumień, aby kod wywołujący oczekiwał i mógł złapać sprawdzony wyjątek.
MarcG,

Odpowiedzi:

250

Prosta odpowiedź na twoje pytanie brzmi: nie możesz, przynajmniej nie bezpośrednio. I to nie twoja wina. Oracle zawiedli to. Trzymają się koncepcji sprawdzonych wyjątków, ale niekonsekwentnie zapomnieli się zająć sprawdzonymi wyjątkami przy projektowaniu interfejsów funkcjonalnych, strumieni, lambda itp. To wszystko na młyn ekspertów takich jak Robert C. Martin, którzy nazywają sprawdzone wyjątki nieudanym eksperymentem.

Moim zdaniem jest to ogromny błąd w interfejsie API i niewielki błąd w specyfikacji języka .

Błąd w interfejsie API polega na tym, że nie zapewnia on możliwości przekazywania sprawdzonych wyjątków, w przypadku których byłoby to naprawdę sensowne dla programowania funkcjonalnego. Jak pokażę poniżej, taki obiekt byłby łatwo możliwy.

Błąd w specyfikacji języka polega na tym, że nie pozwala on parametrowi wnioskować o liście typów zamiast jednego typu, o ile parametr typu jest używany tylko w sytuacjach, w których lista typów jest dopuszczalna ( throwsklauzula).

Jako programiści Java oczekujemy, że następujący kod powinien się skompilować:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class CheckedStream {
    // List variant to demonstrate what we actually had before refactoring.
    public List<Class> getClasses(final List<String> names) throws ClassNotFoundException {
        final List<Class> classes = new ArrayList<>();
        for (final String name : names)
            classes.add(Class.forName(name));
        return classes;
    }

    // The Stream function which we want to compile.
    public Stream<Class> getClasses(final Stream<String> names) throws ClassNotFoundException {
        return names.map(Class::forName);
    }
}

Daje to jednak:

cher@armor1:~/playground/Java/checkedStream$ javac CheckedStream.java 
CheckedStream.java:13: error: incompatible thrown types ClassNotFoundException in method reference
        return names.map(Class::forName);
                         ^
1 error

Sposób, w jaki interfejsy funkcjonalne są określone obecnie uniemożliwia Compiler z przekazaniem wyjątek - nie ma deklaracji, która powie Stream.map(), że jeśli Function.apply() throws E, Stream.map() throws Ejak również.

Brakuje deklaracji parametru typu do przekazywania sprawdzonych wyjątków. Poniższy kod pokazuje, w jaki sposób taki parametr typu tranzytowego mógł zostać zadeklarowany przy obecnej składni. Z wyjątkiem specjalnego przypadku w zaznaczonej linii, który jest limitem omówionym poniżej, ten kod kompiluje się i zachowuje zgodnie z oczekiwaniami.

import java.io.IOException;
interface Function<T, R, E extends Throwable> {
    // Declare you throw E, whatever that is.
    R apply(T t) throws E;
}   

interface Stream<T> {
    // Pass through E, whatever mapper defined for E.
    <R, E extends Throwable> Stream<R> map(Function<? super T, ? extends R, E> mapper) throws E;
}   

class Main {
    public static void main(final String... args) throws ClassNotFoundException {
        final Stream<String> s = null;

        // Works: E is ClassNotFoundException.
        s.map(Class::forName);

        // Works: E is RuntimeException (probably).
        s.map(Main::convertClass);

        // Works: E is ClassNotFoundException.
        s.map(Main::throwSome);

        // Doesn't work: E is Exception.
        s.map(Main::throwSomeMore);  // error: unreported exception Exception; must be caught or declared to be thrown
    }   

    public static Class convertClass(final String s) {
        return Main.class;
    }   

    static class FooException extends ClassNotFoundException {}

    static class BarException extends ClassNotFoundException {}

    public static Class throwSome(final String s) throws FooException, BarException {
        throw new FooException();
    }   

    public static Class throwSomeMore(final String s) throws ClassNotFoundException, IOException  {
        throw new FooException();
    }   
}   

W przypadku throwSomeMorechcielibyśmy IOExceptionbyć przeoczeni, ale tak naprawdę tęsknimy Exception.

Nie jest to idealne, ponieważ wnioskowanie typu wydaje się szukać pojedynczego typu, nawet w przypadku wyjątków. Ponieważ wnioskowanie o typie wymaga jednego typu, Emusi zostać rozpoznane jako wspólne superz ClassNotFoundExceptioni IOException, co jest Exception.

Konieczna jest poprawka definicji wnioskowania o typie, aby kompilator szukał wielu typów, jeśli parametr type jest używany tam, gdzie dozwolona jest lista typów ( throwsklauzula). Wówczas typ wyjątku zgłoszony przez kompilator byłby tak konkretny jak oryginalna throwsdeklaracja sprawdzonych wyjątków metody referencyjnej, a nie pojedynczy supertyp typu catch-all.

Zła wiadomość jest taka, że ​​oznacza to, że Oracle popełniło błąd. Z pewnością nie złamią kodu użytkownika-lądu, ale wprowadzenie parametrów typu wyjątku do istniejących interfejsów funkcjonalnych przerwałoby kompilację całego kodu użytkownika-użytkownika, który używa tych interfejsów jawnie. Będą musieli wymyślić nowy cukier składniowy, aby to naprawić.

Jeszcze gorszą wiadomością jest to, że ten temat był już omawiany przez Briana Goetza w 2010 r. Https://blogs.oracle.com/briangoetz/entry/exception_transparency_in_java (nowy link: http://mail.openjdk.java.net/pipermail/lambda -dev / 2010-June / 001484.html ), ale jestem poinformowany, że to dochodzenie ostatecznie się nie potoczyło i że w Oracle nie ma obecnie żadnych prac, które mogłyby złagodzić interakcje między sprawdzonymi wyjątkami a lambdas.

Christian Hujer
źródło
16
Ciekawy. Uważam, że niektórzy cenią strumienie za łatwiejszy kod równoległy, a inni za czystszy kod. Brian Goetz najwyraźniej bardziej dba o paralelizm (ponieważ jest autorem Java Concurrency in Practice), podczas gdy Robert Martin dba bardziej o czysty kod (ponieważ jest autorem książki Clean Code). Try / catch Boilerplate to niewielka cena za równoległość, więc nic dziwnego, że Brian Goetz nie jest przerażony problemami z użyciem sprawdzonych wyjątków w strumieniach. Nic dziwnego, że Robert Martin nienawidzi sprawdzanych wyjątków, ponieważ dodają bałaganu.
MarcG,
5
Przewiduję, że za kilka lat trudność radzenia sobie ze sprawdzonymi wyjątkami wewnątrz strumieni doprowadzi do jednego z tych dwóch rezultatów: ludzie przestaną używać sprawdzonych wyjątków, LUB wszyscy zaczną używać hacka bardzo podobnego do tego, który zamieściłem moja odpowiedź UtilException. Założę się, że strumienie Java-8 są ostatnim gwóźdźem do trumny sprawdzonych wyjątków, gdyby nie fakt, że sprawdzone wyjątki są częścią JDK. Chociaż lubię i używam sprawdzonych wyjątków w kodzie biznesowym (dla niektórych szczególnych przypadków użycia), wolałbym, żeby wszystkie popularne wyjątki JDK były rozszerzone.
MarcG,
9
@Unihedro Pozostaje problem, że interfejsy funkcjonalne nie przekazują wyjątków. Potrzebowałbym try-catchbloku wewnątrz lambdy, a to po prostu nie ma sensu. Gdy tylko Class.forNamezostanie użyta w jakiś sposób w lambda, na przykład w names.forEach(Class::forName), problem już istnieje. Zasadniczo metody, które zgłaszają sprawdzone wyjątki, zostały wykluczone z udziału w programowaniu funkcjonalnym jako interfejsy funkcjonalne bezpośrednio przez (zły!) Projekt.
Christian Hujer
26
@ChristianHujer Eksploracja „wyjątku przejrzystości” była właśnie eksploracją (taką, która powstała w propozycji BGGA). Po głębszej analizie okazało się, że oferuje słabą równowagę wartości i złożoności, i miał pewne poważne problemy (doprowadziły do ​​nierozstrzygniętych problemów wnioskowania, a „złapanie X” było niesłuszne, między innymi.) Bardzo często pomysł językowy jest wydaje się obiecujący - nawet „oczywisty” - ale po głębszej eksploracji okazał się wadliwy. To był jeden z tych przypadków.
Brian Goetz,
13
@BrianGoetz Czy są jakieś dostępne informacje publiczne na temat nierozwiązywalnych problemów wnioskowania, o których wspomniałeś? Jestem ciekawy i chciałbym to zrozumieć.
Christian Hujer
169

Ta LambdaExceptionUtilklasa pomocnicza pozwala używać dowolnych sprawdzonych wyjątków w strumieniach Java, takich jak:

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
      .map(rethrowFunction(Class::forName))
      .collect(Collectors.toList());

Class::forNameRzuty notatek ClassNotFoundException, które są zaznaczone . Sam strumień również zgłasza ClassNotFoundException, a NIE jakiś zawijany niesprawdzony wyjątek.

public final class LambdaExceptionUtil {

@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
    void accept(T t) throws E;
    }

@FunctionalInterface
public interface BiConsumer_WithExceptions<T, U, E extends Exception> {
    void accept(T t, U u) throws E;
    }

@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
    R apply(T t) throws E;
    }

@FunctionalInterface
public interface Supplier_WithExceptions<T, E extends Exception> {
    T get() throws E;
    }

@FunctionalInterface
public interface Runnable_WithExceptions<E extends Exception> {
    void run() throws E;
    }

/** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */
public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
    return t -> {
        try { consumer.accept(t); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E {
    return (t, u) -> {
        try { biConsumer.accept(t, u); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

/** .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName)) */
public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
    return t -> {
        try { return function.apply(t); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */
public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E {
    return () -> {
        try { return function.get(); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** uncheck(() -> Class.forName("xxx")); */
public static void uncheck(Runnable_WithExceptions t)
    {
    try { t.run(); }
    catch (Exception exception) { throwAsUnchecked(exception); }
    }

/** uncheck(() -> Class.forName("xxx")); */
public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
    {
    try { return supplier.get(); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

/** uncheck(Class::forName, "xxx"); */
public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
    try { return function.apply(t); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

@SuppressWarnings ("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }

}

Wiele innych przykładów jego użycia (po statycznym imporcie LambdaExceptionUtil):

@Test
public void test_Consumer_with_checked_exceptions() throws IllegalAccessException {
    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(className -> System.out.println(Class.forName(className))));

    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(System.out::println));
    }

@Test
public void test_Function_with_checked_exceptions() throws ClassNotFoundException {
    List<Class> classes1
          = Stream.of("Object", "Integer", "String")
                  .map(rethrowFunction(className -> Class.forName("java.lang." + className)))
                  .collect(Collectors.toList());

    List<Class> classes2
          = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
                  .map(rethrowFunction(Class::forName))
                  .collect(Collectors.toList());
    }

@Test
public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException {
    Collector.of(
          rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))),
          StringJoiner::add, StringJoiner::merge, StringJoiner::toString);
    }

@Test    
public void test_uncheck_exception_thrown_by_method() {
    Class clazz1 = uncheck(() -> Class.forName("java.lang.String"));

    Class clazz2 = uncheck(Class::forName, "java.lang.String");
    }

@Test (expected = ClassNotFoundException.class)
public void test_if_correct_exception_is_still_thrown_by_method() {
    Class clazz3 = uncheck(Class::forName, "INVALID");
    }    

Uwaga 1: Do rethrowmetody tej LambdaExceptionUtilklasy powyżej mogą być wykorzystywane bez strachu i są OK do zastosowania w każdej sytuacji . Ogromne podziękowania dla użytkownika @PaoloC, który pomógł rozwiązać ostatni problem: teraz kompilator poprosi cię o dodanie klauzul rzucania i wszystko będzie tak, jakbyś mógł nacieszyć sprawdzone wyjątki natywnie na strumieniach Java 8.


UWAGA 2: Do uncheckmetody tej LambdaExceptionUtilklasy powyżej sposoby premiowania i może być bezpiecznie usunięty je z klasy, jeśli nie chcą z nich korzystać. Jeśli ich używałeś, rób to ostrożnie, a nie zanim zrozumiesz następujące przypadki użycia, zalety / wady i ograniczenia:

• Możesz użyć tych uncheckmetod, jeśli wywołujesz metodę, która dosłownie nigdy nie może zgłosić wyjątku, który deklaruje. 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 wyciszyć ją przy minimalnej płycie kotła:String text = uncheck(() -> new String(byteArr, "UTF-8"));

• Możesz użyć tych uncheckmetod, jeśli wdrażasz ścisły interfejs, w którym nie masz możliwości dodania deklaracji rzucania, a mimo to zgłoszenie wyjątku jest całkowicie odpowiednie. Zawijanie wyjątku tylko w celu uzyskania przywileju jego rzucania 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 każdym razie, jeśli zdecydujesz się skorzystać z uncheckmetod, pamiętaj o tych 2 konsekwencjach rzucenia SPRAWDZONYCH wyjątków bez klauzuli wyrzucania: 1) Kod wywołujący nie będzie w stanie złapać go według nazwy (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 i tak może być tym, czego chcesz. 2) Narusza zasadę najmniejszego zaskoczenia: nie wystarczy już złapać, RuntimeExceptionaby zagwarantować złapanie wszystkich możliwych wyjątków. Z tego powodu uważam, że nie należy tego robić w kodzie frameworka, ale tylko w kodzie biznesowym, który całkowicie kontrolujesz.

MarcG
źródło
4
Czuję, że ta odpowiedź została niesprawiedliwie odrzucona. Kod działa. Sprawdzane wyjątki mają być zgłaszane lub rozpatrywane. Jeśli chcesz je wyrzucić, po prostu zachowaj klauzulę „throws” w metodzie zawierającej strumień. Ale jeśli chcesz sobie z nimi poradzić, po prostu pakując i pisząc od nowa, myślę, że wolę użyć powyższego kodu, aby „odznaczyć” wyjątki i pozwolić im same bańkować. Jedyną różnicą, o której zdaję sobie sprawę, jest to, że wyjątek propagacyjny nie wydłuży RuntimeException. Wiem, że purystom się to nie spodoba, ale czy to „nieuchronnie wróci, by kogoś ugryźć”? Nie wydaje się prawdopodobne.
MarcG,
4
@Christian Hujer, jeśli mam być szczery z downvoter, przegłosował poprzednią wersję, zanim dodałem wyjaśnienie „zalety, wady i ograniczenia”. Może wtedy było to zasłużone. Nie możesz nauczyć kogoś, jak łamać zasady bez przynajmniej próby zrozumienia i wyjaśnienia konsekwencji. Głównym powodem, dla którego opublikowałem to pytanie, było uzyskanie opinii na temat wad mojej odpowiedzi. Ostatecznie otrzymałem tę informację zwrotną nie tutaj, ale z innego pytania w programistach.stackexchange. Potem wróciłem tutaj i zaktualizowałem swoją odpowiedź.
MarcG
16
Zanotowałem tylko dlatego, że zachęca to do niemożliwego do utrzymania kodu . To brzydki hack, choć sprytny, i nigdy nie uznam tej odpowiedzi za przydatną. Jest to kolejny kolejny „nie używaj” języka.
Unihedron
12
@Unihedro, ale dlaczego stało się niemożliwe do utrzymania? Nie rozumiem dlaczego. Jakieś przykłady?
MarcG
2
Moim zdaniem @SuppressWarnings ("unchecked")oszustwo kompilatora jest całkowicie niedopuszczalne.
Thorbjørn Ravn Andersen
26

Nie możesz tego zrobić bezpiecznie. Możesz oszukiwać, ale wtedy twój program jest zepsuty i to nieuchronnie wróci, by kogoś ugryźć (to ty powinieneś, ale często nasze oszustwo wysadza kogoś innego).

Oto nieco bezpieczniejszy sposób na zrobienie tego (ale nadal tego nie polecam).

class WrappedException extends RuntimeException {
    Throwable cause;

    WrappedException(Throwable cause) { this.cause = cause; }
}

static WrappedException throwWrapped(Throwable t) {
    throw new WrappedException(t);
}

try 
    source.stream()
          .filter(e -> { ... try { ... } catch (IOException e) { throwWrapped(e); } ... })
          ...
}
catch (WrappedException w) {
    throw (IOException) w.cause;
}

W tym przypadku łapiesz wyjątek na lambda, wyrzucając sygnał z potoku strumienia, który wskazuje, że obliczenia nie powiodły się wyjątkowo, wychwytuje sygnał i działa na podstawie tego sygnału, aby wygenerować wyjątek leżący u podstaw. Kluczem jest to, że zawsze wychwytujesz wyjątek syntetyczny, zamiast pozwalać na wyciekanie sprawdzonego wyjątku bez deklarowania, że ​​wyjątek został zgłoszony.

Brian Goetz
źródło
18
Tylko pytanie; jaka była decyzja projektowa, która spowodowała, że ​​lambdas nie były w stanie propagować sprawdzonych wyjątków poza swoim kontekstem? Zauważ, że rozumiem, że funkcjonalne interfejsy, takie jak Functionetc, nic nie robią throws; Jestem po prostu ciekawy.
fge
4
To throw w.cause;nie zmusiłoby kompilatora do narzekania, że ​​metoda nie rzuca ani nie łapie Throwable? Jest więc prawdopodobne, że IOExceptionpotrzebna byłaby tam obsada . Ponadto, jeśli lambda zgłosi więcej niż jeden typ sprawdzonego wyjątku, treść połowu stałaby się nieco brzydka z niektórymi instanceofkontrolami (lub czymś innym o podobnym celu) w celu sprawdzenia, który sprawdzany wyjątek został zgłoszony.
Victor Stafusa,
10
@schatten Jednym z powodów jest to, że można zapomnieć o złapaniu WE, a wtedy wycieknie dziwny wyjątek (z którym nikt nie wie, jak sobie poradzić). (Możesz powiedzieć „ale złapałeś wyjątek, więc jest bezpieczny.” W tym przykładzie z zabawkami. Ale za każdym razem, gdy widzę, że baza kodów przyjmuje takie podejście, w końcu ktoś zapomina. Pokusa zignorowania wyjątków nie zna granic.) Inne ryzyko oznacza, że ​​bezpieczne korzystanie z niego jest specyficzne dla konkretnej kombinacji (skorzystaj z witryny, wyjątek). Nie skaluje się dobrze do wielu wyjątków lub niejednorodnych zastosowań.
Brian Goetz
2
@hoodaticus Zgadzam się z tobą. Powiedziałeś, czy wolisz owijanie coraz więcej (jak pokazano powyżej, zwiększając ryzyko „zapomnienia”), czy po prostu stwórz 4 sprytne interfejsy i używaj owijania bez lambd, jak pokazano na stackoverflow.com/a/30974991/2365724 ? Dzięki
PaoloC,
10
Szczerze mówiąc, to rozwiązanie jest po prostu całkowicie niewykonalne. Myślałem, że celem strumieni jest zmniejszenie płyty kotłowej, a nie jej zwiększenie.
wvdz
24

Możesz!

Rozszerzanie @marcg UtilExceptioni dodawanie throw Etam, gdzie to konieczne: w ten sposób kompilator poprosi cię o dodanie klauzul rzucania i wszystko będzie tak, jakbyś mógł rzucać sprawdzone wyjątki natywnie w strumieniach Java 8.

Instrukcje: po prostu skopiuj / wklej LambdaExceptionUtildo swojego IDE, a następnie użyj go, jak pokazano poniżej LambdaExceptionUtilTest.

public final class LambdaExceptionUtil {

    @FunctionalInterface
    public interface Consumer_WithExceptions<T, E extends Exception> {
        void accept(T t) throws E;
    }

    @FunctionalInterface
    public interface Function_WithExceptions<T, R, E extends Exception> {
        R apply(T t) throws E;
    }

    /**
     * .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name))));
     */
    public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
        return t -> {
            try {
                consumer.accept(t);
            } catch (Exception exception) {
                throwActualException(exception);
            }
        };
    }

    /**
     * .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName))
     */
    public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E  {
        return t -> {
            try {
                return function.apply(t);
            } catch (Exception exception) {
                throwActualException(exception);
                return null;
            }
        };
    }

    @SuppressWarnings("unchecked")
    private static <E extends Exception> void throwActualException(Exception exception) throws E {
        throw (E) exception;
    }

}

Niektóre testy pokazujące użycie i zachowanie:

public class LambdaExceptionUtilTest {

    @Test(expected = MyTestException.class)
    public void testConsumer() throws MyTestException {
        Stream.of((String)null).forEach(rethrowConsumer(s -> checkValue(s)));
    }

    private void checkValue(String value) throws MyTestException {
        if(value==null) {
            throw new MyTestException();
        }
    }

    private class MyTestException extends Exception { }

    @Test
    public void testConsumerRaisingExceptionInTheMiddle() {
        MyLongAccumulator accumulator = new MyLongAccumulator();
        try {
            Stream.of(2L, 3L, 4L, null, 5L).forEach(rethrowConsumer(s -> accumulator.add(s)));
            fail();
        } catch (MyTestException e) {
            assertEquals(9L, accumulator.acc);
        }
    }

    private class MyLongAccumulator {
        private long acc = 0;
        public void add(Long value) throws MyTestException {
            if(value==null) {
                throw new MyTestException();
            }
            acc += value;
        }
    }

    @Test
    public void testFunction() throws MyTestException {
        List<Integer> sizes = Stream.of("ciao", "hello").<Integer>map(rethrowFunction(s -> transform(s))).collect(toList());
        assertEquals(2, sizes.size());
        assertEquals(4, sizes.get(0).intValue());
        assertEquals(5, sizes.get(1).intValue());
    }

    private Integer transform(String value) throws MyTestException {
        if(value==null) {
            throw new MyTestException();
        }
        return value.length();
    }

    @Test(expected = MyTestException.class)
    public void testFunctionRaisingException() throws MyTestException {
        Stream.of("ciao", null, "hello").<Integer>map(rethrowFunction(s -> transform(s))).collect(toList());
    }

}
PaoloC
źródło
1
Przepraszamy, masz rację, po prostu dodaj <Integer>wcześniej map. W rzeczywistości kompilator Java nie może wywnioskować Integertypu zwrotu. Wszystko inne powinno być poprawne.
PaoloC,
1
To zadziałało dla mnie. Dzięki temu odpowiedź MarcG była idealna, wymuszając obsługę wyjątku.
Skychan
1
Rozwiązanie powyższego problemu: Zadeklaruj zmienną taką jak to Wyrażenie <ThingType> Konsumenta = rethrowConsumer ((ThingType thing) -> thing.clone ()); następnie użyj tego wyrażenia w wewnętrznym foreach.
Skychan
1
@Skychan: Ponieważ w tej zmodyfikowanej nowej wersji nie wyklucza się już żadnych wyjątków, prawdopodobnie system wnioskowania jest nieco trudniejszy. W komentarzu poniżej Brian Goetz mówi o „przejrzystości wyjątku” prowadzącej do „nierozstrzygniętych problemów wnioskowania”.
MarcG,
3
Bardzo dobrze. Jedyną niefortunną rzeczą jest to, że nie działa idealnie z metodą, która generuje wiele sprawdzonych wyjątków. W takim przypadku kompilator sprawi, że złapiesz wspólny nadtyp, np Exception.
wvdz
12

Wystarczy użyć dowolnego z NoException (mojego projektu), niezaznaczonego jOOλ , lambda do rzucania , interfejsów Throwable lub Faux Pas .

// NoException
stream.map(Exceptions.sneak().function(Class::forName));

// jOOλ
stream.map(Unchecked.function(Class::forName));

// throwing-lambdas
stream.map(Throwing.function(Class::forName).sneakyThrow());

// Throwable interfaces
stream.map(FunctionWithThrowable.aFunctionThatUnsafelyThrowsUnchecked(Class::forName));

// Faux Pas
stream.map(FauxPas.throwingFunction(Class::forName));
Robert Važan
źródło
7

Napisałem bibliotekę, która rozszerza interfejs API Stream, aby umożliwić zgłaszanie sprawdzonych wyjątków. Wykorzystuje sztuczkę Briana Goetza.

Twój kod stałby się

public List<Class> getClasses() throws ClassNotFoundException {     
    Stream<String> classNames = 
        Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String");

    return ThrowingStream.of(classNames, ClassNotFoundException.class)
               .map(Class::forName)
               .collect(Collectors.toList());
}
Jeffrey
źródło
7

Ta odpowiedź jest podobna do 17, ale unika definicji wyjątku opakowania:

List test = new ArrayList();
        try {
            test.forEach(obj -> {

                //let say some functionality throws an exception
                try {
                    throw new IOException("test");
                }
                catch(Exception e) {
                    throw new RuntimeException(e);
                }
            });
        }
        catch (RuntimeException re) {
            if(re.getCause() instanceof IOException) {
                //do your logic for catching checked
            }
            else 
                throw re; // it might be that there is real runtime exception
        }
Radosław Stoyanov
źródło
1
To proste i skuteczne rozwiązanie.
Lin W
2
Właśnie tego nie chciała Op: wypróbować bloki w lambda. Ponadto działa tylko zgodnie z oczekiwaniami, o ile żaden inny kod poza blokiem try nie zawija wyjątku IOException w RuntimeException. Aby tego uniknąć, można użyć niestandardowego opakowania-RuntimeException (zdefiniowanego jako prywatna klasa wewnętrzna).
Malte Hartwig,
5

Nie możesz.

Możesz jednak rzucić okiem na jeden z moich projektów, który pozwala łatwiej manipulować takimi „rzucającymi lambdami”.

W twoim przypadku możesz to zrobić:

import static com.github.fge.lambdas.functions.Functions.wrap;

final ThrowingFunction<String, Class<?>> f = wrap(Class::forName);

List<Class> classes =
    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .map(f.orThrow(MyException.class))
          .collect(Collectors.toList());

i złapać MyException.

To jest jeden przykład. Innym przykładem może być .orReturn()wartość domyślna.

Zauważ, że to jest WCIĄŻ praca w toku, więcej ma nadejść. Lepsze nazwy, więcej funkcji itp.

fge
źródło
2
Ale jeśli chcesz wyrzucić oryginalny sprawdzony wyjątek, musisz dodać try / catch wokół strumienia, aby go rozpakować, co nadal jest okropne! Podoba mi się pomysł, że MOŻESZ rzucić niesprawdzony wyjątek, jeśli chcesz, i MUSISZ zwrócić domyślną wartość do strumienia, jeśli chcesz, ale myślę również, że powinieneś dodać jakąś .orThrowChecked()metodę do swojego projektu, która pozwala na rzucenie sprawdzonego wyjątku . Proszę spojrzeć na moją UtilExceptionodpowiedź na tej stronie i zobaczyć, czy podoba wam się pomysł dodania trzeciej możliwości do swojego projektu.
MarcG
„Ale jeśli chcesz rzucić oryginalny sprawdzony wyjątek, musisz dodać try / catch wokół strumienia, aby go rozpakować, co nadal jest okropne!” <- tak, ale nie masz wyboru. Lambdas nie może propagować sprawdzonych wyjątków poza swoim kontekstem, jest to „decyzja” projektowa (osobiście uważam to za wadę
fge
Co do twojego pomysłu, nie bardzo rozumiem, co on robi, przepraszam; mimo wszystko wciąż rzucasz jako niezaznaczone, więc czym to się różni od tego, co robię? (z wyjątkiem tego, że mam inny interfejs)
fge
W każdym razie zapraszamy do udziału w projekcie! Zauważyłeś też, że to Streamimplementuje AutoCloseable?
fge
Pozwól, że cię zapytam: czy MyExceptionpowyższe informacje muszą być niesprawdzonym wyjątkiem?
MarcG
3

Podsumowując powyższe komentarze, zaawansowane rozwiązanie polega na użyciu specjalnego opakowania dla niesprawdzonych funkcji z konstruktorem takim jak API, który zapewnia odzyskiwanie, ponowne wysyłanie i supresję.

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .map(Try.<String, Class<?>>safe(Class::forName)
                  .handle(System.out::println)
                  .unsafe())
          .collect(toList());

Poniższy kod demonstruje to dla interfejsów konsumenta, dostawcy i funkcji. Można go łatwo rozszerzyć. Niektóre publiczne słowa kluczowe zostały usunięte w tym przykładzie.

Klasa Try jest punktem końcowym dla kodu klienta. Bezpieczne metody mogą mieć unikalną nazwę dla każdego typu funkcji. CheckedConsumer , CheckedSupplier i CheckedFunction to sprawdzone analogi funkcji lib, których można używać niezależnie od Try

CheckedBuilder to interfejs do obsługi wyjątków w niektórych sprawdzonych funkcjach. orTry pozwala wykonać inną funkcję tego samego typu, jeśli poprzednie nie powiodło się. uchwyt zapewnia obsługę wyjątków, w tym filtrowanie typów wyjątków. Kolejność programów obsługi jest ważna. Sposoby zmniejszenia niebezpiecznych i przekaż ponownie generuje ostatni powód w łańcuchu wykonania. Zredukuj metody orElse i orElseGet zwracają wartość alternatywną, taką jak Opcjonalne, jeśli wszystkie funkcje zawiodły. Istnieje również metoda tłumienia . CheckedWrapper jest powszechną implementacją CheckedBuilder.

final class Try {

    public static <T> CheckedBuilder<Supplier<T>, CheckedSupplier<T>, T> 
        safe(CheckedSupplier<T> supplier) {
        return new CheckedWrapper<>(supplier, 
                (current, next, handler, orResult) -> () -> {
            try { return current.get(); } catch (Exception ex) {
                handler.accept(ex);
                return next.isPresent() ? next.get().get() : orResult.apply(ex);
            }
        });
    }

    public static <T> Supplier<T> unsafe(CheckedSupplier<T> supplier) {
        return supplier;
    }

    public static <T> CheckedBuilder<Consumer<T>, CheckedConsumer<T>, Void> 
        safe(CheckedConsumer<T> consumer) {
        return new CheckedWrapper<>(consumer, 
                (current, next, handler, orResult) -> t -> {
            try { current.accept(t); } catch (Exception ex) {
                handler.accept(ex);
                if (next.isPresent()) {
                    next.get().accept(t);
                } else {
                    orResult.apply(ex);
                }
            }
        });
    }

    public static <T> Consumer<T> unsafe(CheckedConsumer<T> consumer) {
        return consumer;
    }

    public static <T, R> CheckedBuilder<Function<T, R>, CheckedFunction<T, R>, R> 
        safe(CheckedFunction<T, R> function) {
        return new CheckedWrapper<>(function, 
                (current, next, handler, orResult) -> t -> {
            try { return current.applyUnsafe(t); } catch (Exception ex) {
                handler.accept(ex);
                return next.isPresent() ? next.get().apply(t) : orResult.apply(ex);
            }
        });
    }

    public static <T, R> Function<T, R> unsafe(CheckedFunction<T, R> function) {
        return function;
    }

    @SuppressWarnings ("unchecked")
    static <T, E extends Throwable> T throwAsUnchecked(Throwable exception) throws E { 
        throw (E) exception; 
    }
}

@FunctionalInterface interface CheckedConsumer<T> extends Consumer<T> {
    void acceptUnsafe(T t) throws Exception;
    @Override default void accept(T t) {
        try { acceptUnsafe(t); } catch (Exception ex) {
            Try.throwAsUnchecked(ex);
        }
    }
}

@FunctionalInterface interface CheckedFunction<T, R> extends Function<T, R> {
    R applyUnsafe(T t) throws Exception;
    @Override default R apply(T t) {
        try { return applyUnsafe(t); } catch (Exception ex) {
            return Try.throwAsUnchecked(ex);
        }
    }
}

@FunctionalInterface interface CheckedSupplier<T> extends Supplier<T> {
    T getUnsafe() throws Exception;
    @Override default T get() {
        try { return getUnsafe(); } catch (Exception ex) {
            return Try.throwAsUnchecked(ex);
        }
    }
}

interface ReduceFunction<TSafe, TUnsafe, R> {
    TSafe wrap(TUnsafe current, Optional<TSafe> next, 
            Consumer<Throwable> handler, Function<Throwable, R> orResult);
}

interface CheckedBuilder<TSafe, TUnsafe, R> {
    CheckedBuilder<TSafe, TUnsafe, R> orTry(TUnsafe next);

    CheckedBuilder<TSafe, TUnsafe, R> handle(Consumer<Throwable> handler);

    <E extends Throwable> CheckedBuilder<TSafe, TUnsafe, R> handle(
            Class<E> exceptionType, Consumer<E> handler);

    CheckedBuilder<TSafe, TUnsafe, R> handleLast(Consumer<Throwable> handler);

    <E extends Throwable> CheckedBuilder<TSafe, TUnsafe, R> handleLast(
            Class<E> exceptionType, Consumer<? super E> handler);

    TSafe unsafe();
    TSafe rethrow(Function<Throwable, Exception> transformer);
    TSafe suppress();
    TSafe orElse(R value);
    TSafe orElseGet(Supplier<R> valueProvider);
}

final class CheckedWrapper<TSafe, TUnsafe, R> 
        implements CheckedBuilder<TSafe, TUnsafe, R> {

    private final TUnsafe function;
    private final ReduceFunction<TSafe, TUnsafe, R> reduceFunction;

    private final CheckedWrapper<TSafe, TUnsafe, R> root;
    private CheckedWrapper<TSafe, TUnsafe, R> next;

    private Consumer<Throwable> handlers = ex -> { };
    private Consumer<Throwable> lastHandlers = ex -> { };

    CheckedWrapper(TUnsafe function, 
            ReduceFunction<TSafe, TUnsafe, R> reduceFunction) {
        this.function = function;
        this.reduceFunction = reduceFunction;
        this.root = this;
    }

    private CheckedWrapper(TUnsafe function, 
            CheckedWrapper<TSafe, TUnsafe, R> prev) {
        this.function = function;
        this.reduceFunction = prev.reduceFunction;
        this.root = prev.root;
        prev.next = this;
    }

    @Override public CheckedBuilder<TSafe, TUnsafe, R> orTry(TUnsafe next) {
        return new CheckedWrapper<>(next, this);
    }

    @Override public CheckedBuilder<TSafe, TUnsafe, R> handle(
            Consumer<Throwable> handler) {
        handlers = handlers.andThen(handler);
        return this;
    }

    @Override public <E extends Throwable> CheckedBuilder<TSafe, TUnsafe, R> 
        handle(Class<E> exceptionType, Consumer<E> handler) {
        handlers = handlers.andThen(ex -> {
            if (exceptionType.isInstance(ex)) {
                handler.accept(exceptionType.cast(ex));
            }
        });
        return this;
    }

    @Override public CheckedBuilder<TSafe, TUnsafe, R> handleLast(
            Consumer<Throwable> handler) {
        lastHandlers = lastHandlers.andThen(handler);
        return this;
    }

    @Override public <E extends Throwable> CheckedBuilder<TSafe, TUnsafe, R> 
        handleLast(Class<E> exceptionType, Consumer<? super E> handler) {
        lastHandlers = lastHandlers.andThen(ex -> {
            if (exceptionType.isInstance(ex)) {
                handler.accept(exceptionType.cast(ex));
            }
        });
        return this;
    }

    @Override public TSafe unsafe() {
        return root.reduce(ex -> Try.throwAsUnchecked(ex));
    }

    @Override
    public TSafe rethrow(Function<Throwable, Exception> transformer) {
        return root.reduce(ex -> Try.throwAsUnchecked(transformer.apply(ex)));
    }

    @Override public TSafe suppress() {
        return root.reduce(ex -> null);
    }

    @Override public TSafe orElse(R value) {
        return root.reduce(ex -> value);
    }

    @Override public TSafe orElseGet(Supplier<R> valueProvider) {
        Objects.requireNonNull(valueProvider);
        return root.reduce(ex -> valueProvider.get());
    }

    private TSafe reduce(Function<Throwable, R> orResult) {
        return reduceFunction.wrap(function, 
                Optional.ofNullable(next).map(p -> p.reduce(orResult)), 
                this::handle, orResult);
    }

    private void handle(Throwable ex) {
        for (CheckedWrapper<TSafe, TUnsafe, R> current = this; 
                current != null; 
                current = current.next) {
            current.handlers.accept(ex);
        }
        lastHandlers.accept(ex);
    }
}
introspekcyjny
źródło
3

TL; DR Wystarczy użyć Lomboka@SneakyThrows .

Christian Hujer wyjaśnił już szczegółowo, dlaczego rzucanie sprawdzonych wyjątków ze strumienia jest, ściśle mówiąc, niemożliwe z powodu ograniczeń Javy.

Niektóre inne odpowiedzi wyjaśniły sztuczki, które pozwalają ominąć ograniczenia języka, ale nadal są w stanie spełnić wymóg rzucenia „samego sprawdzonego wyjątku i bez dodawania brzydkiego try / catch do strumienia” , niektóre z nich wymagają dziesiątek dodatkowych wierszy płyty kotłowej.

Podkreślę inną opcję robienia tego, że IMHO jest znacznie czystsze niż wszystkie inne: Lombok @SneakyThrows. Zostało to wspomniane przy okazji innych odpowiedzi, ale zostało nieco pochowane pod wieloma niepotrzebnymi szczegółami.

Wynikowy kod jest tak prosty, jak:

public List<Class> getClasses() throws ClassNotFoundException {
    List<Class> classes =
        Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
                .map(className -> getClass(className))
                .collect(Collectors.toList());
    return classes;
}

@SneakyThrows                                 // <= this is the only new code
private Class<?> getClass(String className) {
    return Class.forName(className);
}

Potrzebowaliśmy tylko jednego Extract Methodrefaktoryzacji (wykonanego przez IDE) i jednej dodatkowej linii dla @SneakyThrows. Adnotacja dba o dodanie całej tablicy kontrolnej, aby upewnić się, że możesz rzucić sprawdzony wyjątek bez zawijania go RuntimeExceptioni bez konieczności jawnego deklarowania go.

sergut
źródło
4
Używanie lomboka powinno być odradzane.
Dragas,
2

Możesz także napisać metodę otoki, aby otoczyć niezaznaczone wyjątki, a nawet ulepszyć otoki o dodatkowy parametr reprezentujący inny interfejs funkcjonalny (o tym samym typie zwrotu R ). W takim przypadku możesz przekazać funkcję, która byłaby wykonana i zwrócona w przypadku wyjątków. Zobacz przykład poniżej:

private void run() {
    List<String> list = Stream.of(1, 2, 3, 4).map(wrapper(i ->
            String.valueOf(++i / 0), i -> String.valueOf(++i))).collect(Collectors.toList());
    System.out.println(list.toString());
}

private <T, R, E extends Exception> Function<T, R> wrapper(ThrowingFunction<T, R, E> function, 
Function<T, R> onException) {
    return i -> {
        try {
            return function.apply(i);
        } catch (ArithmeticException e) {
            System.out.println("Exception: " + i);
            return onException.apply(i);
        } catch (Exception e) {
            System.out.println("Other: " + i);
            return onException.apply(i);
        }
    };
}

@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
    R apply(T t) throws E;
}
Piotr Niewiński
źródło
2

Oto inne spojrzenie lub rozwiązanie pierwotnego problemu. Tutaj pokazuję, że mamy opcję napisania kodu, który będzie przetwarzał tylko prawidłowy podzbiór wartości z opcją wykrywania i obsługi przypadków, gdy wyjątek został zgłoszony.

    @Test
    public void getClasses() {

        String[] classNames = {"java.lang.Object", "java.lang.Integer", "java.lang.Foo"};
        List<Class> classes =
                Stream.of(classNames)
                        .map(className -> {
                            try {
                                return Class.forName(className);
                            } catch (ClassNotFoundException e) {
                                // log the error
                                return null;
                            }
                        })
                        .filter(c -> c != null)
                        .collect(Collectors.toList());

        if (classes.size() != classNames.length) {
            // add your error handling here if needed or process only the resulting list
            System.out.println("Did not process all class names");
        }

        classes.forEach(System.out::println);
    }
OSGI Java
źródło
1

Zgadzam się z powyższymi komentarzami, używając Stream.map jesteś ograniczony do implementacji Funkcji, która nie zgłasza Wyjątków.

Możesz jednak stworzyć swój własny interfejs FunctionalInterface, który będzie wyrzucany jak poniżej.

@FunctionalInterface
public interface UseInstance<T, X extends Throwable> {
  void accept(T instance) throws X;
}

następnie zaimplementuj go za pomocą Lambdas lub odnośników, jak pokazano poniżej.

import java.io.FileWriter;
import java.io.IOException;

//lambda expressions and the execute around method (EAM) pattern to
//manage resources

public class FileWriterEAM  {
  private final FileWriter writer;

  private FileWriterEAM(final String fileName) throws IOException {
    writer = new FileWriter(fileName);
  }
  private void close() throws IOException {
    System.out.println("close called automatically...");
    writer.close();
  }
  public void writeStuff(final String message) throws IOException {
    writer.write(message);
  }
  //...

  public static void use(final String fileName, final UseInstance<FileWriterEAM, IOException> block) throws IOException {

    final FileWriterEAM writerEAM = new FileWriterEAM(fileName);    
    try {
      block.accept(writerEAM);
    } finally {
      writerEAM.close();
    }
  }

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

    FileWriterEAM.use("eam.txt", writerEAM -> writerEAM.writeStuff("sweet"));

    FileWriterEAM.use("eam2.txt", writerEAM -> {
        writerEAM.writeStuff("how");
        writerEAM.writeStuff("sweet");      
      });

    FileWriterEAM.use("eam3.txt", FileWriterEAM::writeIt);     

  }


 void writeIt() throws IOException{
     this.writeStuff("How ");
     this.writeStuff("sweet ");
     this.writeStuff("it is");

 }

}
JohnnyO
źródło
1

Jedynym wbudowanym sposobem obsługi sprawdzonych wyjątków, które mogą zostać wygenerowane przez mapoperację, jest hermetyzowanie ich w pliku CompletableFuture. (An Optionaljest prostszą alternatywą, jeśli nie musisz zachowywać wyjątku.) Klasy te mają na celu umożliwienie reprezentowania operacji warunkowych w sposób funkcjonalny.

Wymaganych jest kilka nietrywialnych metod pomocniczych, ale można uzyskać kod, który jest stosunkowo zwięzły, a jednocześnie wyraźnie widać, że wynik strumienia zależy od mappomyślnego zakończenia operacji. Oto jak to wygląda:

    CompletableFuture<List<Class<?>>> classes =
            Stream.of("java.lang.String", "java.lang.Integer", "java.lang.Double")
                  .map(MonadUtils.applyOrDie(Class::forName))
                  .map(cfc -> cfc.thenApply(Class::getSuperclass))
                  .collect(MonadUtils.cfCollector(ArrayList::new,
                                                  List::add,
                                                  (List<Class<?>> l1, List<Class<?>> l2) -> { l1.addAll(l2); return l1; },
                                                  x -> x));
    classes.thenAccept(System.out::println)
           .exceptionally(t -> { System.out.println("unable to get class: " + t); return null; });

Daje to następujące dane wyjściowe:

[class java.lang.Object, class java.lang.Number, class java.lang.Number]

applyOrDieSposób zajmuje Functionktóry zgłasza wyjątek i zamienia go na Functionktóre zwraca już zakończona CompletableFuture- albo zakończony normalnie wyniku pierwotnej funkcji, albo zakończone wyłącznie ze rzucony wyjątku.

Druga mapoperacja pokazuje, że masz teraz Stream<CompletableFuture<T>>zamiast Stream<T>. CompletableFuturezajmuje się wykonaniem tej operacji, tylko jeśli operacja poprzedzająca zakończyła się powodzeniem. API sprawia, że ​​jest to oczywiste, ale stosunkowo bezbolesne.

Dopóki nie przejdziesz do collectfazy. Tutaj potrzebujemy dość znaczącej metody pomocniczej. Chcemy „lift” normalnej operacji kolekcji (w tym przypadku toList()) „wewnątrz” CompletableFuture- cfCollector()pozwala nam to zrobić za pomocą supplier, accumulator, combineri finisherże nie trzeba nic wiedzieć co chodzi CompletableFuture.

Metody pomocnicze można znaleźć w GitHub w mojej MonadUtilsklasie, która wciąż jest w toku.

Matt McHenry
źródło
1

Prawdopodobnie lepszym i bardziej funkcjonalnym sposobem jest zawijanie wyjątków i propagowanie ich dalej w strumieniu. Spójrz na Try typu Vavr na przykład.

Przykład:

interface CheckedFunction<I, O> {
    O apply(I i) throws Exception; }

static <I, O> Function<I, O> unchecked(CheckedFunction<I, O> f) {
    return i -> {
        try {
            return f.apply(i);
        } catch(Exception ex) {

            throw new RuntimeException(ex);
        }
    } }

fileNamesToRead.map(unchecked(file -> Files.readAllLines(file)))

LUB

@SuppressWarnings("unchecked")
private static <T, E extends Exception> T throwUnchecked(Exception e) throws E {
    throw (E) e;
}

static <I, O> Function<I, O> unchecked(CheckedFunction<I, O> f) {
    return arg -> {
        try {
            return f.apply(arg);
        } catch(Exception ex) {
            return throwUnchecked(ex);
        }
    };
}

Druga implementacja pozwala uniknąć zawijania wyjątku w pliku RuntimeException. throwUncheckeddziała, ponieważ prawie zawsze wszystkie wyjątki ogólne są w java traktowane jako niezaznaczone.

Michaił Chołodkow
źródło
1

Używam tego rodzaju wyjątku zawijania:

public class CheckedExceptionWrapper extends RuntimeException {
    ...
    public <T extends Exception> CheckedExceptionWrapper rethrow() throws T {
        throw (T) getCause();
    }
}

Będzie to wymagało statycznego obchodzenia się z tymi wyjątkami:

void method() throws IOException, ServletException {
    try { 
        list.stream().forEach(object -> {
            ...
            throw new CheckedExceptionWrapper(e);
            ...            
        });
    } catch (CheckedExceptionWrapper e){
        e.<IOException>rethrow();
        e.<ServletExcepion>rethrow();
    }
}

Wypróbuj online!

Chociaż wyjątek zostanie zresztą ponownie zgłoszony podczas pierwszego rethrow()wywołania (och, generyczne Java ...), ten sposób pozwala uzyskać ścisłą statyczną definicję możliwych wyjątków (wymaga ich deklaracji throws). I nic nie instanceofjest potrzebne.

Taras
źródło
-1

Myślę, że to podejście jest właściwe:

public List<Class> getClasses() throws ClassNotFoundException {
    List<Class> classes;
    try {
        classes = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String").map(className -> {
            try {
                return Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new UndeclaredThrowableException(e);
            }
        }).collect(Collectors.toList());
    } catch (UndeclaredThrowableException e) {
        if (e.getCause() instanceof ClassNotFoundException) {
            throw (ClassNotFoundException) e.getCause();
        } else {
            // this should never happen
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
    return classes;
}

Owijanie sprawdzony wyjątek wewnątrz Callablew AUndeclaredThrowableException (tak jest w przypadku stosowania dla tego wyjątku) i rozpakowanie go na zewnątrz.

Tak, uważam to za brzydkie i odradzam używanie lambdas w tym przypadku i po prostu powrócę do starej dobrej pętli, chyba że pracujesz z równoległym strumieniem, a paralellizacja przynosi obiektywną korzyść, która uzasadnia nieczytelność kodu.

Jak wielu innych zauważyło, istnieją rozwiązania tej sytuacji i mam nadzieję, że jedna z nich przekształci się w przyszłą wersję Javy.

Matthias Ronge
źródło
1
(1) Istnieje już kilka odpowiedzi pokazujących taki przykład, więc co twoja odpowiedź dodaje do pytań i odpowiedzi, które nie zostały jeszcze uwzględnione? Publikowanie podobnych odpowiedzi po prostu dodaje bałaganu do witryny. (2) OP wyraźnie mówi, że nie chce tego robić. „Uwaga: NIE chcę zawijać zaznaczonego wyjątku w wyjątku środowiska wykonawczego i zamiast tego rzucać zawinięty, niesprawdzony wyjątek”.
Radiodef,