Nić.sleep Infinite while loop w lambda nie wymaga 'catch (InterruptedException)' - dlaczego nie?
Moje pytanie dotyczy InterruptedException
, które jest rzucane metodą Thread.sleep
. Podczas pracy z ExecutorService
zauważyłem dziwne zachowanie, którego nie rozumiem; oto co mam na myśli:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(true)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
Z tym kodem kompilator nie daje mi żadnego błędu ani komunikatu, który InterruptedException
z Thread.sleep
powinien zostać przechwycony. Ale kiedy próbuję zmienić warunek pętli i zastąpić "true" jakąś zmienną taką jak ta:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(tasksObserving)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
Kompilator ciągle narzeka, że {[2] } musi być obsługiwany. Czy ktoś może mi wyjaśnić dlaczego tak się dzieje i dlaczego jeśli warunek jest ustawiony na true kompilator ignoruje InterruptedException?
2 answers
Powodem tego jest to, że wywołania te są w rzeczywistości wywołaniami dwóch różnych przeciążonych metod dostępnych w ExecutorService
; każda z tych metod przyjmuje jeden argument różnych typów:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
Wtedy kompilator konwertuje lambda w pierwszym przypadku twojego problemu do funkcjonalnego interfejsu Callable<?>
(wywołując pierwszą przeciążoną metodę); a w drugim przypadku twojego problemu konwertuje lambda do funkcjonalnego interfejsu Runnable
(wywołując tym samym drugą metodę przeciążoną), wymagając z tego powodu obsługi Exception
rzuconego; ale nie w poprzednim przypadku używając Callable
.
Chociaż oba interfejsy funkcjonalne nie pobierają żadnych argumentów, Callable<?>
Zwraca wartość:
- Callable:
V call() throws Exception;
- Runnable:
public abstract void run();
Jeśli przełączymy się na przykłady, które przycinają kod do odpowiednich elementów (do łatwo zbadać tylko ciekawe bity) wtedy możemy napisać, równoważnie z oryginalnymi przykładami:
ExecutorService executor = Executors.newSingleThreadExecutor();
// LAMBDA COMPILED INTO A 'Callable<?>'
executor.submit(() -> {
while (true)
throw new Exception();
});
// LAMBDA COMPILED INTO A 'Runnable': EXCEPTIONS MUST BE HANDLED BY LAMBDA ITSELF!
executor.submit(() -> {
boolean value = true;
while (value)
throw new Exception();
});
Z tych przykładów można łatwiej zauważyć, że powodem, dla którego pierwszy z nich jest konwertowany na Callable<?>
, podczas gdy drugi jest konwertowany na Runnable
, jest wnioskowanie kompilatora .
W obu przypadkach ciała lambda są kompatybilne z void , ponieważ każde polecenie return w bloku ma postać return;
.
Teraz, w pierwszym w przypadku, kompilator wykonuje następujące czynności:
- wykrywa, że wszystkie ścieżki wykonania w lambdzie deklarują rzucanie sprawdzonych WYJĄTKÓW (od teraz będziemy nazywać 'wyjątkami' , sugerując tylko 'sprawdzonymi wyjątkami' ). Obejmuje to wywołanie dowolnej metody deklarującej wyjątki rzucania oraz wywołanie jawne
throw new <CHECKED_EXCEPTION>()
. - konkluduje poprawnie, że całość ciała lambda jest równoznaczna z blokiem kodu deklarującym rzucanie wyjątków; które oczywiście musi być albo: obsłużone, albo ponownie rzucone.
- ponieważ lambda nie obsługuje wyjątku, kompilator domyślnie zakłada, że te wyjątki muszą zostać ponownie wyrzucone.
- bezpiecznie wnioskuje, że ta lambda musi pasować do interfejsu funkcjonalnego, który nie może
complete normally
i dlatego jest zgodny z wartością. - ponieważ
Callable<?>
iRunnable
są potencjalnymi dopasowaniami dla tej lambdy, kompilator wybiera najbardziej konkretne dopasowanie (aby pokryć wszystkie scenariusze); czyliCallable<?>
, przekształcając lambda w jego instancję i tworząc odwołanie do metodysubmit(Callable<?>)
overloaded.
Podczas gdy w drugim przypadku kompilator wykonuje następujące czynności:
- wykrywa, że w lambdzie mogą być ścieżki wykonania, które nie deklarują WYJĄTKÓW rzucania (w zależności od do-be-evaluated logic ).
- ponieważ nie wszystkie ścieżki wykonawcze deklarują wyjątki rzucania, kompilator kończy że ciało lambda jest niekoniecznie odpowiednikiem bloku kodu deklarującego wyrzucanie WYJĄTKÓW - kompilator nie dba/zwraca uwagę, czy niektóre części kodu deklarują, że mogą, tylko jeśli całe ciało to robi lub nie.
- bezpiecznie wywnioskować, że lambda nie jest zgodna z wartością ; ponieważ może
complete normally
. - wybiera
Runnable
(ponieważ jest to jedyny dostępny dopasowanie funkcjonalny interfejs do konwersji lambda) i tworzy odwołanie do metodysubmit(Runnable)
overloaded. Wszystko to za cenę przekazania użytkownikowi odpowiedzialności za obsługę wszelkichException
s wyrzuconych gdziekolwiek mogą wystąpić w częściach ciała lambda.
To było świetne pytanie - świetnie się bawiłem, dzięki!
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-09-07 21:05:43
Krótko
ExecutorService
posiada zarówno metody submit(Callable)
jak i submit(Runnable)
.
- w pierwszym przypadku (z
while (true)
) obasubmit(Callable)
isubmit(Runnable)
pasują, więc kompilator musi wybrać pomiędzy nimi-
submit(Callable)
jest wybierane przezsubmit(Runnable)
PonieważCallable
jest bardziej szczegółowe niżRunnable
-
Callable
mAthrows Exception
Wcall()
, więc nie jest konieczne przechwytywanie wyjątku wewnątrz
-
- w drugim przypadku (z
while (tasksObserving)
) Tylkosubmit(Runnable)
pasuje, więc kompilator wybiera Informatyka-
Runnable
nie posiada deklaracjithrows
dotyczącej swojej metodyrun()
, więc błędem kompilacji jest nie przechwycenie wyjątku wewnątrz metodyrun()
.
-
Pełna historia
Specyfikacja języka Java opisuje sposób wyboru metody podczas kompilacji programu w $15.2.2 :
- określić potencjalnie stosowane metody ($15.12.2.1) który odbywa się w 3 fazach dla ścisłej, luźnej i zmiennej arytmetyki inwokacja
- wybierz najbardziej konkretną metodę ($15.12.2.5) z metod znalezionych na pierwszym kroku.
Przeanalizujmy sytuację za pomocą 2 metod submit()
w dwóch fragmentach kodu dostarczonych przez OP:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(true)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
I
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(tasksObserving)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
(gdzie tasksObserving
nie jest zmienną końcową).
Identyfikacja Potencjalnie Stosowanych Metod
Najpierw kompilator musi zidentyfikować potencjalnie stosowane metody: $15.12.2.1
Jeśli element jest stałą arytmetyką metody z arytmetyką n, arytmetyka wywołania metody jest równa n i dla wszystkich i (1 ≤ i ≤ n), i argument wywołania metody jest potencjalnie zgodny , jak określono poniżej, z typem i ' - tego parametru metody.
I nieco dalej w tej samej sekcji
Wyrażenie jest potencjalnie kompatybilne z typem docelowym zgodnie z następującymi zasadami:
Wyrażenie lambda (§15.27) jest potencjalnie kompatybilny z funkcjonalnym typem interfejsu (§9.8), jeśli wszystkie z poniższych są prawdziwe:
Arytmetyka funkcji typu docelowego jest taka sama jak arytmetyka wyrażenia lambda.
Jeśli typ funkcji typu docelowego ma zwrot void, to ciało lambda jest wyrażeniem instrukcji (§14.8) lub blokiem zgodnym z void (§15.27.2).
Jeśli typ funkcji typu docelowego ma typ zwracany (non-void), to ciało lambda jest albo wyrażenie lub blok zgodny z wartością (§15.27.2).
Zauważmy, że w obu przypadkach lambda jest blokiem lambda.
Zauważmy również, że Runnable
mA typ zwracania void
, więc aby być potencjalnie kompatybilnym z Runnable
, blok lambda musi być blokiem kompatybilnym z void. W tym samym czasie Callable
mA typ non-void return, więc aby być potencjalnie comtatible z Callable
, blok lambda musi być zgodny z wartością blok .
$15.27.2 definiuje czym są Void-compatible-block i value-compatible-block .
Blok lambda body jest kompatybilny z void, jeśli każde polecenie return w bloku ma postać
return;
.Blok lambda jest zgodny z wartością, jeśli nie może normalnie się wypełnić (§14.21), a każda instrukcja return w bloku ma postać
return Expression;
.
Spójrzmy na $14.21, paragraf o while
pętli:
W przypadku borh lambda to w rzeczywistości lambda blokowe.A while twierdzenie może być wypełnione normalnie iff co najmniej jedna z następujących wartości jest prawdziwa:
Twierdzenie while jest osiągalne, a wyrażenie warunkowe nie jest wyrażeniem stałym (§15.28) o wartości true.
Istnieje osiągalna instrukcja break, która kończy instrukcję while.
W pierwszym przypadku, jak widać, istnieje while
pętla ze stałym wyrażeniem o wartości true
(BEZ break
poleceń), więc nie może wypełnić normalnie (o $14.21); nie ma też poleceń return, stąd pierwsza lambda jest zgodna z wartością.
W tym samym czasie, nie ma return
w ogóle, więc jest równieżVoid-compatible . W pierwszym przypadku lambda jest kompatybilna zarówno z void, jak i z wartością.
W drugim przypadku pętla while
może zakończyć się normalnie z punktu widzenia kompilatora (ponieważ pętla wyrażenie nie jest już wyrażeniem stałym), więc lambda w całości może zakończyć się normalnie, więc jest nie a blok zgodny z wartością . Ale nadal jest blokiem kompatybilnym z void , ponieważ nie zawiera return
instrukcji.
Przypominając to, co zauważyliśmy wcześniej, oznacza to, że w pierwszym przypadku lambda będzie potencjalnie kompatybilna zarówno z Callable
, jak i Runnable
; w drugim przypadku lambda będzie tylko potencjalnie kompatybilna z Runnable
.
Wybierz najbardziej konkretną metodę
W pierwszym przypadku kompilator musi wybrać pomiędzy tymi dwoma metodami, ponieważ obie są potencjalnie odpowiednie . Robi to za pomocą procedura o nazwie "wybierz najbardziej konkretną metodę" i opisana w $15.12.2.5. Oto fragment:
Interfejs funkcjonalny typu S jest bardziej specyficzny niż interfejs funkcjonalny typu T dla wyrażenia e, Jeśli T nie jest podtypem S i jedno z poniższych jest prawdziwe (gdzie U1 ... Uk i R1 to typy parametrów i typ powrotu funkcji typu przechwytywania S, A V1 ... Vk i R2 są typami parametrów i typami zwrotnymi funkcji typu T):
Jeśli e jest wyrażenie lambda (§15.27.1), wtedy jedno z poniższych jest prawdziwe:
R2 jest nieważny.
Po pierwsze,
Wyrażenie lambda z zerowymi parametrami jest jawnie typowane.
Również żadne z Runnable
i Callable
nie jest podklasą siebie, a Runnable
typem zwracanym jest void
, więc mamy dopasowanie: Callable
jest bardziej szczegółowy niż Runnable
. Oznacza to, że pomiędzy submit(Callable)
A submit(Runnable)
w pierwszym przypadku metoda z Callable
zostanie wybrany.
Jeśli chodzi o drugi przypadek, mamy tylko jedną potencjalnie stosowaną metodę, submit(Runnable)
, więc jest ona wybrana.
Więc dlaczego zmienia się powierzchnia?
W końcu widzimy, że w tych przypadkach kompilator wybiera różne metody. W pierwszym przypadku wnioskuje się, że lambda jest Callable
, która ma {[13] } na swojej metodzie call()
, tak że sleep()
kompiluje wywołania. W drugim przypadku, to Runnable
, który run()
nie deklaruje żadnego throwable wyjątki, więc kompilator skarży się, że wyjątek nie zostanie złapany.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55