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?

Author: user207421, 2019-05-15

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:

  1. <T> Future<T> submit(Callable<T> task);
  2. 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ść:

  1. Callable: V call() throws Exception;
  2. 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:

  1. 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>().
  2. 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.
  3. ponieważ lambda nie obsługuje wyjątku, kompilator domyślnie zakłada, że te wyjątki muszą zostać ponownie wyrzucone.
  4. bezpiecznie wnioskuje, że ta lambda musi pasować do interfejsu funkcjonalnego, który nie może complete normally i dlatego jest zgodny z wartością.
  5. ponieważ Callable<?> i Runnable są potencjalnymi dopasowaniami dla tej lambdy, kompilator wybiera najbardziej konkretne dopasowanie (aby pokryć wszystkie scenariusze); czyli Callable<?>, przekształcając lambda w jego instancję i tworząc odwołanie do metody submit(Callable<?>) overloaded.

Podczas gdy w drugim przypadku kompilator wykonuje następujące czynności:

  1. 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 ).
  2. 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.
  3. bezpiecznie wywnioskować, że lambda nie jest zgodna z wartością ; ponieważ może complete normally.
  4. wybiera Runnable (ponieważ jest to jedyny dostępny dopasowanie funkcjonalny interfejs do konwersji lambda) i tworzy odwołanie do metody submit(Runnable) overloaded. Wszystko to za cenę przekazania użytkownikowi odpowiedzialności za obsługę wszelkich Exceptions wyrzuconych gdziekolwiek mogą wystąpić w częściach ciała lambda.

To było świetne pytanie - świetnie się bawiłem, dzięki!

 62
Author: Marco R.,
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).

  1. w pierwszym przypadku (z while (true)) oba submit(Callable) i submit(Runnable) pasują, więc kompilator musi wybrać pomiędzy nimi
    • submit(Callable) jest wybierane przez submit(Runnable) Ponieważ Callable jest bardziej szczegółowe niż Runnable
    • Callable mA throws Exception W call(), więc nie jest konieczne przechwytywanie wyjątku wewnątrz
  2. w drugim przypadku (z while (tasksObserving)) Tylko submit(Runnable) pasuje, więc kompilator wybiera Informatyka
    • Runnable nie posiada deklaracji throws dotyczącej swojej metody run(), więc błędem kompilacji jest nie przechwycenie wyjątku wewnątrz metody run().

Pełna historia

Specyfikacja języka Java opisuje sposób wyboru metody podczas kompilacji programu w $15.2.2 :

  1. 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
  2. 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:

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 przypadku borh lambda to w rzeczywistości lambda blokowe.

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.

W pierwszym przypadku lambda jest zarówno blokiem kompatybilnym z voidem, jak i blokiem kompatybilnym z wartością; w drugim przypadku jest to tylko a blok kompatybilny z void .

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.

 3
Author: Roman Puchkovskiy,
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