Thread.sleep внутри бесконечного, в то время как цикл в лямбде не требует 'catch (InterruptedException)' - почему бы и нет?

Мой вопрос о InterruptedException, который вызывается из метода Thread.sleep. Во время работы с ExecutorService я заметил странное поведение, которое я не понимаю; вот что я имею в виду:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

С этим кодом компилятор не Thread.sleep мне ни ошибки, ни сообщения о том, что InterruptedException из Thread.sleep должен быть Thread.sleep. Но когда я пытаюсь изменить условие цикла и заменить "true" на некоторую переменную, подобную этой:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

Компилятор постоянно жалуется, что InterruptedException должен быть обработан. Может кто-нибудь объяснить мне, почему это происходит, и почему, если условие имеет значение true, компилятор игнорирует InterruptedException?

Ответы

Ответ 1

Причина этого заключается в том, что эти вызовы фактически являются вызовами двух разных перегруженных методов, доступных в ExecutorService; каждый из этих методов принимает один аргумент разных типов:

  1. <T> Future<T> submit(Callable<T> task);
  2. Future<?> submit(Runnable task);

Затем происходит то, что компилятор преобразует лямбду в первом случае вашей проблемы в функциональный интерфейс Callable<?> (вызывая первый перегруженный метод); и во втором случае вашей проблемы преобразует лямбду в функциональный интерфейс Runnable (вызывая поэтому второй перегруженный метод), требуя из-за этого обрабатывать брошенный Exception; но не в предыдущем случае, используя Callable.

Хотя оба функциональных интерфейса не принимают никаких аргументов, Callable<?> возвращает значение:

  1. Вызывается: V call() throws Exception;
  2. Runnable: public abstract void run();

Если мы переключимся на примеры, которые обрезают код до соответствующих частей (чтобы легко исследовать только любопытные биты), то мы можем написать, эквивалентно оригинальным примерам:

    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();
    });

С этими примерами может быть легче заметить, что причина, по которой первый преобразуется в Callable<?>, а второй преобразуется в Runnable, заключается в выводах компилятора.

В обоих случаях лямбда-тела являются void-совместимыми, поскольку каждый оператор возврата в блоке имеет форму return;.

Теперь в первом случае компилятор делает следующее:

  1. Обнаруживает, что все пути выполнения в лямбда-выражении объявляют выбрасывание проверенных исключений (отныне мы будем называть "исключение", подразумевая только "проверенные исключения"). Это включает вызов любого метода, объявляющего бросающие исключения, и явный вызов throw new <CHECKED_EXCEPTION>().
  2. Правильно приходит к выводу, что ВСЕ тело лямбды эквивалентно блоку кода, объявляющему исключения исключения; который, конечно, ДОЛЖЕН быть: обработан или переброшен.
  3. Так как лямбда не обрабатывает исключение, компилятор по умолчанию полагает, что эти исключения должны быть переброшены.
  4. Безопасно сделать вывод, что эта лямбда должна соответствовать функциональному интерфейсу, не может complete normally и поэтому совместима по значению.
  5. Поскольку Callable<?> и Runnable являются потенциальными совпадениями для этой лямбды, компилятор выбирает наиболее конкретное совпадение (чтобы охватить все сценарии); это Callable<?>, преобразующий лямбду в его экземпляр и создающий ссылку на вызов перегруженного метода submit(Callable<?>).

Хотя во втором случае компилятор выполняет следующие действия:

  1. Обнаруживает, что в лямбде могут быть пути выполнения, которые НЕ объявляют исключения (в зависимости от логики, подлежащей оценке).
  2. Поскольку не во всех путях выполнения объявляются исключения-исключения, компилятор приходит к выводу, что тело лямбды НЕ ОБЯЗАТЕЛЬНО эквивалентно блоку кода, объявляющему исключения-броски - компилятор не заботится и не обращает внимания на некоторые части кода заявлять, что они могут, только если все тело делает или нет.
  3. Безопасно делает вывод, что лямбда не совместима по значению; так как МОЖЕТ complete normally.
  4. Выбирает Runnable (поскольку это единственный доступный функциональный интерфейс для лямбда-преобразования, в который необходимо преобразовать лямбду) и создает ссылку на вызов перегруженного метода submit(Runnable). Все это происходит за счет делегирования пользователю ответственности за обработку любого Exception броска, где бы он не мог происходить в пределах частей лямбда-тела.

Это был замечательный вопрос - мне было очень весело гоняться за ним, спасибо!

Ответ 2

кратко

ExecutorService имеет методы submit(Callable) и submit(Runnable).

  1. В первом случае (с while (true)) оба submit(Callable) и submit(Runnable) совпадают, поэтому компилятору приходится выбирать между ними
    • submit(Callable) выбирается submit(Callable) submit(Runnable) потому что Callable более специфичен, чем Runnable
    • Callable есть throws Exception в call(), поэтому нет необходимости перехватывать исключение внутри него.
  2. Во втором случае (при использовании while (tasksObserving)) только submit(Runnable), поэтому компилятор выбирает его
    • Runnable не имеет объявления throws в своем методе run(), поэтому это ошибка компиляции, которая не перехватывает исключение внутри метода run().

Полная история

Спецификация языка Java описывает, как метод выбирается во время компиляции программы в $ 15.2.2:

  1. Определите потенциально применимые методы ($ 15.12.2.1), что делается в 3 этапа для строгого, свободного и переменного вызова арности
  2. Выберите наиболее специфичный метод ($ 15.12.2.5) из методов, найденных на первом шаге.

Давайте проанализируем ситуацию с двумя методами submit() в двух фрагментах кода, предоставленных OP:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

а также

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

(где tasksObserving не является окончательной переменной).

Определить потенциально применимые методы

Во-первых, компилятор должен определить потенциально применимые методы: $ 15.12.2.1

Если член является методом фиксированной арности с арностью n, арность вызова метода равна n, и для всех я (1 ≤ я ≤ n) i-й аргумент вызова метода потенциально совместим, как определено ниже, с типом i-го параметра метода.

и чуть дальше в том же разделе

Выражение потенциально совместимо с целевым типом в соответствии со следующими правилами:

Лямбда-выражение (§15.27) потенциально совместимо с функциональным типом интерфейса (§9.8), если выполняются все следующие условия:

Арность типа функции целевого типа такая же, как арность лямбда-выражения.

Если тип функции целевого типа имеет возврат void, то лямбда-тело является либо выражением оператора (§14.8), либо void- совместимым блоком (§15.27.2).

Если у типа функции целевого типа есть тип возвращаемого значения (не пустого), то лямбда-тело является либо выражением, либо блоком, совместимым со значением (§15.27.2).

Отметим, что в обоих случаях лямбда является блочной лямбда.

Также отметим, что Runnable имеет тип возврата void, поэтому, чтобы быть потенциально совместимым с Runnable, лямбда-блок должен быть void- совместимым блоком. В то же время Callable имеет тип возврата, Callable от void, поэтому, чтобы быть потенциально сопоставимым с Callable, лямбда-блок должен быть блоком, совместимым со значениями.

$ 15.27.2 определяет, что такое void- совместимый блок и значение-совместимый блок.

Лямбда-тело блока совместимо с void-, если каждый оператор return в блоке имеет форму return; ,

Лямбда-тело блока является совместимым по значению, если не может нормально завершиться (§14.21), и каждый оператор возврата в блоке имеет форму return Expression; ,

Пусть смотрят на $ 14,21, пункт о в while цикла:

Оператор while может обычно завершаться, если хотя бы одно из следующих условий верно:

Оператор while достижим, и выражение условия не является константным выражением (§15.28) со значением true.

Существует оператор достижимого прерывания, который выходит из оператора while.

В некоторых случаях лямбды на самом деле являются блочными лямбдами.

В первом случае, как можно видеть, есть в while цикл с постоянным выражением со значением true (без break заявлений), поэтому он не может завершить normallly (по $ 14,21); также он не имеет операторов возврата, следовательно, первая лямбда является совместимой по значению.

В то же время, нет никаких операторов return, поэтому он также совместим с void-. Итак, в конце, в первом случае, лямбда является void- и совместимой по значению.

Во втором случае, в while цикл может завершаться нормально с точки зрения компилятора (потому что выражение цикла не является постоянным выражение больше), так что лямбда в полном объеме может завершаться нормально, так что это не является значением, совместимым блок. Но это все еще совместимый void- блок, потому что он не содержит операторов return.

Промежуточный результат заключается в том, что в первом случае лямбда является одновременно void- совместимым блоком и совместимым по значению блоком; во втором случае это только void- совместимый блок.

Вспоминая то, что мы отметили ранее, это означает, что в первом случае лямбда будет потенциально совместима как с Callable и с Runnable; во втором случае лямбда будет потенциально совместима только с Runnable.

Выберите наиболее конкретный метод

В первом случае компилятор должен выбрать один из двух методов, потому что оба они потенциально применимы. Это делается с помощью процедуры под названием "Выберите наиболее специфический метод", описанной в $ 15.12.2.5. Вот выдержка:

Тип функционального интерфейса S более специфичен, чем тип функционального интерфейса T для выражения e, если T не является подтипом S и выполняется одно из следующих условий (где U1... Uk и R1 - типы параметров и возвращаемый тип тип функции захвата S, а V1... Vk и R2 являются типами параметров и типом возврата типа функции T):

Если e является лямбда-выражением с явным типом (§15.27.1), то выполняется одно из следующих условий:

R2 недействителен.

Прежде всего,

Лямбда-выражение с нулевыми параметрами явно набрано.

Кроме того, ни один из Runnable и Callable является подкласс друга от друга, и Runnable типа возвращаемого значения void, поэтому мы имеем матч: Callable более специфично, чем Runnable. Это означает, что между submit(Callable) и submit(Runnable) в первом случае будет выбран метод с Callable.

Что касается второго случая, у нас есть только один потенциально применимый метод submit(Runnable), поэтому он выбран.

Так почему же поверхность изменения?

Итак, в конце мы видим, что в этих случаях компилятором выбираются разные методы. В первом случае, лямбда- Callable как Callable который Callable throws Exception в своем методе call(), так что вызов sleep() компилируется. Во втором случае, это Runnable который run() не объявляет какие-либо бросаемые исключения, поэтому компилятор жалуется на то, что исключение не было перехвачено.

Ответ 3

кратко

ExecutorService имеет методы submit(Callable) и submit(Runnable).

  1. В первом случае (с while (true)) оба submit(Callable) и submit(Runnable) совпадают, поэтому компилятору приходится выбирать между ними
    • submit(Callable) выбирается submit(Callable) submit(Runnable) потому что Callable более специфичен, чем Runnable
    • Callable есть throws Exception в invoke(), поэтому нет необходимости перехватывать исключение внутри него.
  2. Во втором случае (при использовании while (tasksObserving)) только submit(Runnable), поэтому компилятор выбирает его
    • Runnable не имеет объявления throws в своем методе run(), поэтому это ошибка компиляции, которая не перехватывает исключение внутри метода run().

Полная история

Спецификация языка Java описывает, как метод выбирается во время компиляции программы в $ 15.2.2:

  1. Определите потенциально применимые методы ($ 15.12.2.1), что делается в 3 этапа для строгого, свободного и переменного вызова арности
  2. Выберите наиболее специфичный метод ($ 15.12.2.5) из методов, найденных на первом шаге.

Давайте проанализируем ситуацию с двумя методами submit() в двух фрагментах кода, предоставленных OP:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(true)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

а также

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        while(tasksObserving)
        {
            //DO SOMETHING
            Thread.sleep(5000);
        }
    });

Определить потенциально применимые методы

Во-первых, компилятор должен определить потенциально применимые методы: $ 15.12.2.1

Если член является методом фиксированной арности с арностью n, арность вызова метода равна n, и для всех я (1 ≤ я ≤ n) i-й аргумент вызова метода потенциально совместим, как определено ниже, с типом i-го параметра метода.

и чуть дальше в том же разделе

Выражение потенциально совместимо с целевым типом в соответствии со следующими правилами:

Лямбда-выражение (§15.27) потенциально совместимо с функциональным типом интерфейса (§9.8), если выполняются все следующие условия:

Арность типа функции целевого типа такая же, как арность лямбда-выражения.

Если тип функции целевого типа имеет возврат void, то лямбда-тело является либо выражением оператора (§14.8), либо void- совместимым блоком (§15.27.2).

Если у типа функции целевого типа есть тип возвращаемого значения (не пустого), то лямбда-тело является либо выражением, либо блоком, совместимым со значением (§15.27.2).

Отметим, что в обоих случаях лямбда является блочной лямбда.

Также отметим, что Runnable имеет тип возврата void, поэтому, чтобы быть потенциально совместимым с Runnable, лямбда-блок должен быть void- совместимым блоком. В то же время Callable имеет тип возврата, Callable от void, поэтому, чтобы быть потенциально сопоставимым с Callable, лямбда-блок должен быть блоком, совместимым со значениями.

$ 15.27.2 определяет, что такое void- совместимый блок и значение-совместимый блок.

Лямбда-тело блока совместимо с void-, если каждый оператор return в блоке имеет форму return; ,

Лямбда-тело блока является совместимым по значению, если не может нормально завершиться (§14.21), и каждый оператор возврата в блоке имеет форму return Expression; ,

Пусть смотрят на $ 14,21, пункт о в while цикла:

Оператор while может обычно завершаться, если хотя бы одно из следующих условий верно:

Оператор while достижим, и выражение условия не является константным выражением (§15.28) со значением true.

Существует оператор достижимого прерывания, который выходит из оператора while.

В некоторых случаях лямбды на самом деле являются блочными лямбдами.

В первом случае, как можно видеть, есть в while цикл с постоянным выражением со значением true (без break заявлений), поэтому он не может завершить normallly (по $ 14,21); также он не имеет операторов возврата, следовательно, первая лямбда является совместимой по значению.

В то же время, нет никаких операторов return, поэтому он также совместим с void-. Итак, в конце, в первом случае, лямбда является void- и совместимой по значению.

Во втором случае, в while цикл может завершаться нормально с точки зрения компилятора (потому что выражение цикла не является постоянным выражение больше), так что лямбда в полном объеме может завершаться нормально, так что это не является значением, совместимым блок. Но это все еще совместимый void- блок, потому что он не содержит операторов return.

Промежуточный результат заключается в том, что в первом случае лямбда является одновременно void- совместимым блоком и совместимым по значению блоком; во втором случае это только void- совместимый блок.

Вспоминая то, что мы отметили ранее, это означает, что в первом случае лямбда будет потенциально совместима как с Callable и с Runnable; во втором случае лямбда будет потенциально совместима только с Runnable.

Выберите наиболее конкретный метод

В первом случае компилятор должен выбрать один из двух методов, потому что оба они потенциально применимы. Это делается с помощью процедуры под названием "Выберите наиболее специфический метод", описанной в $ 15.12.2.5. Вот выдержка:

Тип функционального интерфейса S более специфичен, чем тип функционального интерфейса T для выражения e, если T не является подтипом S и выполняется одно из следующих условий (где U1... Uk и R1 - типы параметров и возвращаемый тип тип функции захвата S, а V1... Vk и R2 являются типами параметров и типом возврата типа функции T):

Если e является лямбда-выражением с явным типом (§15.27.1), то выполняется одно из следующих условий:

R2 недействителен.

Прежде всего,

Лямбда-выражение с нулевыми параметрами явно набрано.

Кроме того, ни один из Runnable и Callable является подкласс друга от друга, и Runnable типа возвращаемого значения void, поэтому мы имеем матч: Callable более специфично, чем Runnable. Это означает, что между submit(Callable) и submit(Runnable) в первом случае будет выбран метод с Callable.

Что касается второго случая, у нас есть только один потенциально применимый метод submit(Runnable), поэтому он выбран.

Так почему же поверхность изменения?

Итак, в конце мы видим, что в этих случаях компилятором выбираются разные методы. В первом случае, лямбда- Callable как Callable который Callable throws Exception в своем методе invoke(), так что вызов sleep() компилируется. Во втором случае, это Runnable который run() не объявляет какие-либо бросаемые исключения, поэтому компилятор жалуется на то, что исключение не было перехвачено.