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
/ catches
do strumienia.
throws ClassNotFoundException
metodę w deklaracji metody zawierającej strumień, aby kod wywołujący oczekiwał i mógł złapać sprawdzony wyjątek.Odpowiedzi:
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 (
throws
klauzula).Jako programiści Java oczekujemy, że następujący kod powinien się skompilować:
Daje to jednak:
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śliFunction.apply() throws E
,Stream.map() throws E
jak 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.
W przypadku
throwSomeMore
chcielibyśmyIOException
być przeoczeni, ale tak naprawdę tęsknimyException
.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,
E
musi zostać rozpoznane jako wspólnesuper
zClassNotFoundException
iIOException
, co jestException
.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 (
throws
klauzula). Wówczas typ wyjątku zgłoszony przez kompilator byłby tak konkretny jak oryginalnathrows
deklaracja 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.
źródło
try-catch
bloku wewnątrz lambdy, a to po prostu nie ma sensu. Gdy tylkoClass.forName
zostanie użyta w jakiś sposób w lambda, na przykład wnames.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.Ta
LambdaExceptionUtil
klasa pomocnicza pozwala używać dowolnych sprawdzonych wyjątków w strumieniach Java, takich jak:Class::forName
Rzuty notatekClassNotFoundException
, które są zaznaczone . Sam strumień również zgłaszaClassNotFoundException
, a NIE jakiś zawijany niesprawdzony wyjątek.Wiele innych przykładów jego użycia (po statycznym imporcie
LambdaExceptionUtil
):Uwaga 1: Do
rethrow
metody tejLambdaExceptionUtil
klasy 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
uncheck
metody tejLambdaExceptionUtil
klasy 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
uncheck
metod, 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
uncheck
metod, 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
uncheck
metod, 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ć,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, ale tylko w kodzie biznesowym, który całkowicie kontrolujesz.źródło
@SuppressWarnings ("unchecked")
oszustwo kompilatora jest całkowicie niedopuszczalne.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).
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.
źródło
Function
etc, nic nie robiąthrows
; Jestem po prostu ciekawy.throw w.cause;
nie zmusiłoby kompilatora do narzekania, że metoda nie rzuca ani nie łapieThrowable
? Jest więc prawdopodobne, żeIOException
potrzebna 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órymiinstanceof
kontrolami (lub czymś innym o podobnym celu) w celu sprawdzenia, który sprawdzany wyjątek został zgłoszony.Możesz!
Rozszerzanie @marcg
UtilException
i dodawaniethrow E
tam, 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
LambdaExceptionUtil
do swojego IDE, a następnie użyj go, jak pokazano poniżejLambdaExceptionUtilTest
.Niektóre testy pokazujące użycie i zachowanie:
źródło
<Integer>
wcześniejmap
. W rzeczywistości kompilator Java nie może wywnioskowaćInteger
typu zwrotu. Wszystko inne powinno być poprawne.Exception
.Wystarczy użyć dowolnego z NoException (mojego projektu), niezaznaczonego jOOλ , lambda do rzucania , interfejsów Throwable lub Faux Pas .
źródło
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ę
źródło
Ta odpowiedź jest podobna do 17, ale unika definicji wyjątku opakowania:
źródło
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ć:
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.
źródło
.orThrowChecked()
metodę do swojego projektu, która pozwala na rzucenie sprawdzonego wyjątku . Proszę spojrzeć na mojąUtilException
odpowiedź na tej stronie i zobaczyć, czy podoba wam się pomysł dodania trzeciej możliwości do swojego projektu.Stream
implementujeAutoCloseable
?MyException
powyższe informacje muszą być niesprawdzonym wyjątkiem?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ę.
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.
źródło
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:
Potrzebowaliśmy tylko jednego
Extract Method
refaktoryzacji (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 goRuntimeException
i bez konieczności jawnego deklarowania go.źródło
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:
źródło
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.
źródło
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.
następnie zaimplementuj go za pomocą Lambdas lub odnośników, jak pokazano poniżej.
źródło
Jedynym wbudowanym sposobem obsługi sprawdzonych wyjątków, które mogą zostać wygenerowane przez
map
operację, jest hermetyzowanie ich w plikuCompletableFuture
. (AnOptional
jest 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
map
pomyślnego zakończenia operacji. Oto jak to wygląda:Daje to następujące dane wyjściowe:
applyOrDie
Sposób zajmujeFunction
który zgłasza wyjątek i zamienia go naFunction
które zwraca już zakończonaCompletableFuture
- albo zakończony normalnie wyniku pierwotnej funkcji, albo zakończone wyłącznie ze rzucony wyjątku.Druga
map
operacja pokazuje, że masz terazStream<CompletableFuture<T>>
zamiastStream<T>
.CompletableFuture
zajmuje 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
collect
fazy. Tutaj potrzebujemy dość znaczącej metody pomocniczej. Chcemy „lift” normalnej operacji kolekcji (w tym przypadkutoList()
) „wewnątrz”CompletableFuture
-cfCollector()
pozwala nam to zrobić za pomocąsupplier
,accumulator
,combiner
ifinisher
że nie trzeba nic wiedzieć co chodziCompletableFuture
.Metody pomocnicze można znaleźć w GitHub w mojej
MonadUtils
klasie, która wciąż jest w toku.źródło
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:
LUB
Druga implementacja pozwala uniknąć zawijania wyjątku w pliku
RuntimeException
.throwUnchecked
działa, ponieważ prawie zawsze wszystkie wyjątki ogólne są w java traktowane jako niezaznaczone.źródło
Używam tego rodzaju wyjątku zawijania:
Będzie to wymagało statycznego obchodzenia się z tymi wyjątkami:
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 deklaracjithrows
). I nic nieinstanceof
jest potrzebne.źródło
Myślę, że to podejście jest właściwe:
Owijanie sprawdzony wyjątek wewnątrz
Callable
w 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.
źródło