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
; каждый из этих методов принимает один аргумент разных типов:
-
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
Затем происходит то, что компилятор преобразует лямбду в первом случае вашей проблемы в функциональный интерфейс Callable<?>
(вызывая первый перегруженный метод); и во втором случае вашей проблемы преобразует лямбду в функциональный интерфейс Runnable
(вызывая поэтому второй перегруженный метод), требуя из-за этого обрабатывать брошенный Exception
; но не в предыдущем случае, используя Callable
.
Хотя оба функциональных интерфейса не принимают никаких аргументов, Callable<?>
возвращает значение:
-
Вызывается:
V call() throws Exception;
- 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;
.
Теперь в первом случае компилятор делает следующее:
- Обнаруживает, что все пути выполнения в лямбда-выражении объявляют выбрасывание проверенных исключений (отныне мы будем называть "исключение", подразумевая только "проверенные исключения"). Это включает вызов любого метода, объявляющего бросающие исключения, и явный вызов
throw new <CHECKED_EXCEPTION>()
.
- Правильно приходит к выводу, что ВСЕ тело лямбды эквивалентно блоку кода, объявляющему исключения исключения; который, конечно, ДОЛЖЕН быть: обработан или переброшен.
- Так как лямбда не обрабатывает исключение, компилятор по умолчанию полагает, что эти исключения должны быть переброшены.
- Безопасно сделать вывод, что эта лямбда должна соответствовать функциональному интерфейсу, не может
complete normally
и поэтому совместима по значению.
- Поскольку
Callable<?>
и Runnable
являются потенциальными совпадениями для этой лямбды, компилятор выбирает наиболее конкретное совпадение (чтобы охватить все сценарии); это Callable<?>
, преобразующий лямбду в его экземпляр и создающий ссылку на вызов перегруженного метода submit(Callable<?>)
.
Хотя во втором случае компилятор выполняет следующие действия:
- Обнаруживает, что в лямбде могут быть пути выполнения, которые НЕ объявляют исключения (в зависимости от логики, подлежащей оценке).
- Поскольку не во всех путях выполнения объявляются исключения-исключения, компилятор приходит к выводу, что тело лямбды НЕ ОБЯЗАТЕЛЬНО эквивалентно блоку кода, объявляющему исключения-броски - компилятор не заботится и не обращает внимания на некоторые части кода заявлять, что они могут, только если все тело делает или нет.
- Безопасно делает вывод, что лямбда не совместима по значению; так как МОЖЕТ
complete normally
.
- Выбирает
Runnable
(поскольку это единственный доступный функциональный интерфейс для лямбда-преобразования, в который необходимо преобразовать лямбду) и создает ссылку на вызов перегруженного метода submit(Runnable)
. Все это происходит за счет делегирования пользователю ответственности за обработку любого Exception
броска, где бы он не мог происходить в пределах частей лямбда-тела.
Это был замечательный вопрос - мне было очень весело гоняться за ним, спасибо!
Ответ 2
кратко
ExecutorService
имеет методы submit(Callable)
и submit(Runnable)
.
- В первом случае (с
while (true)
) оба submit(Callable)
и submit(Runnable)
совпадают, поэтому компилятору приходится выбирать между ними -
submit(Callable)
выбирается submit(Callable)
submit(Runnable)
потому что Callable
более специфичен, чем Runnable
-
Callable
есть throws Exception
в call()
, поэтому нет необходимости перехватывать исключение внутри него.
- Во втором случае (при использовании
while (tasksObserving)
) только submit(Runnable)
, поэтому компилятор выбирает его -
Runnable
не имеет объявления throws
в своем методе run()
, поэтому это ошибка компиляции, которая не перехватывает исключение внутри метода run()
.
Полная история
Спецификация языка Java описывает, как метод выбирается во время компиляции программы в $ 15.2.2:
- Определите потенциально применимые методы ($ 15.12.2.1), что делается в 3 этапа для строгого, свободного и переменного вызова арности
- Выберите наиболее специфичный метод ($ 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)
.
- В первом случае (с
while (true)
) оба submit(Callable)
и submit(Runnable)
совпадают, поэтому компилятору приходится выбирать между ними -
submit(Callable)
выбирается submit(Callable)
submit(Runnable)
потому что Callable
более специфичен, чем Runnable
-
Callable
есть throws Exception
в invoke()
, поэтому нет необходимости перехватывать исключение внутри него.
- Во втором случае (при использовании
while (tasksObserving)
) только submit(Runnable)
, поэтому компилятор выбирает его -
Runnable
не имеет объявления throws
в своем методе run()
, поэтому это ошибка компиляции, которая не перехватывает исключение внутри метода run()
.
Полная история
Спецификация языка Java описывает, как метод выбирается во время компиляции программы в $ 15.2.2:
- Определите потенциально применимые методы ($ 15.12.2.1), что делается в 3 этапа для строгого, свободного и переменного вызова арности
- Выберите наиболее специфичный метод ($ 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()
не объявляет какие-либо бросаемые исключения, поэтому компилятор жалуется на то, что исключение не было перехвачено.