My question is about InterruptedException
, which is thrown from the Thread.sleep
method. While working with ExecutorService
I noticed some
The reason for this, is that these invocations are in fact, invocations to two different overloaded methods available in ExecutorService
; each of these methods taking a single argument of different types:
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
Then what happens is that the compiler is converting the lambda in the first case of your problem into a Callable<?>
functional interface (invoking the first overloaded method); and in the second case of your problem converts the lambda into a Runnable
functional interface (invoking therefore the second overloaded method), requiring because of this to handle the Exception
thrown; but not in the previous case using the Callable
.
Although both functional interfaces don't take any arguments, Callable<?>
returns a value:
- Callable:
V call() throws Exception;
- Runnable:
public abstract void run();
If we switch to examples that trim the code to the relevant pieces (to easily investigate just the curious bits) then we can write, equivalently to the original examples:
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();
});
With these examples, it may be easier to observe that the reason why the first one is converted to a Callable<?>
, while the second one is converted to a Runnable
is because of compiler inferences.
In both cases, the lambda bodies are void-compatible, since every return statement in the block has the form return;
.
Now, in the first case, the compiler does the following:
throw new <CHECKED_EXCEPTION>()
.complete normally
and therefore is value-compatible.Callable<?>
and Runnable
are potential matches for this lambda, the compiler selects the most specific match (to cover all scenarios); which is the Callable<?>
, converting the lambda into an instance of it and creating an invocation reference to the submit(Callable<?>)
overloaded method.While, in the second case, the compiler does the following:
complete normally
.Runnable
(as it is the only available fitting functional interface for the lambda to be converted into) and creates an invocation reference to the submit(Runnable)
overloaded method. All this coming at the price of delegating to the user, the responsibility of handling any Exception
s thrown wherever they MAY occur within portions of the lambda body.This was a great question - I had a lot of fun chasing it down, thanks!
ExecutorService
has both submit(Callable)
and submit(Runnable)
methods.
while (true)
), both submit(Callable)
and submit(Runnable)
match, so the compiler has to choose between them
submit(Callable)
is chosen over submit(Runnable)
because Callable
is more specific than Runnable
Callable
has throws Exception
in call()
, so it is not necessary to catch an exception inside itwhile (tasksObserving)
) only submit(Runnable)
match, so the compiler chooses it
Runnable
has no throws
declaration on its run()
method, so it is a compilation error to not catch the exception inside the run()
method.Java Language Specification describes how the method is chosen during program compilation in $15.2.2 :
Let's analyze the situation with 2 submit()
methods in two code snippets provided by the OP:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(true)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
and
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while(tasksObserving)
{
//DO SOMETHING
Thread.sleep(5000);
}
});
(where tasksObserving
is not a final variable).
First, the compiler has to identify the potentially applicable methods: $15.12.2.1
If the member is a fixed arity method with arity n, the arity of the method invocation is equal to n, and for all i (1 ≤ i ≤ n), the i'th argument of the method invocation is potentially compatible, as defined below, with the type of the i'th parameter of the method.
and a bit further in the same section
An expression is potentially compatible with a target type according to the following rules:
A lambda expression (§15.27) is potentially compatible with a functional interface type (§9.8) if all of the following are true:
The arity of the target type's function type is the same as the arity of the lambda expression.
If the target type's function type has a void return, then the lambda body is either a statement expression (§14.8) or a void-compatible block (§15.27.2).
If the target type's function type has a (non-void) return type, then the lambda body is either an expression or a value-compatible block (§15.27.2).
Let's note that in both cases, the lambda is a block lambda.
Let's also note that Runnable
has void
return type, so to be potentially compatible with Runnable
, a block lambda must be void-compatible block. At the same time, Callable
has a non-void return type, so to be potentially comtatible with Callable
, a block lambda must be value-compatible block.
$15.27.2 defines what a void-compatible-block and value-compatible-block are.
A block lambda body is void-compatible if every return statement in the block has the form
return;
.A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form
return Expression;
.
Let's look at $14.21, paragraph about while
loop:
A while statement can complete normally iff at least one of the following is true:
The while statement is reachable and the condition expression is not a constant expression (§15.28) with value true.
There is a reachable break statement that exits the while statement.
In borh cases, lambdas are actually block lambdas.
In the first case, as it can be seen, there is a while
loop with a constant expression with value true
(without break
statements), so it cannot complete normallly (by $14.21); also it has no return statements, hence the first lambda is value-compatible.
At the same time, there are no return
statements at all, so it is also void-compatible. So, in the end, in the first case, the lambda is both void- and value-compatible.
In the second case, the while
loop can complete normally from the point of view of the compiler (because the loop expression is not a constant expression anymore), so the lambda in its entirety can complete normally, so it is not a value-compatible block. But it is still a void-compatible block because it contains no return
statements.
The intermediate result is that in the first case the lambda is both a void-compatible block and a value-compatible block; in the second case it is only a void-compatible block.
Recalling what we noted earlier, this means that in the first case, the lambda will be potentially compatible both with Callable
and Runnable
; in the second case, the lambda will only be potentially compatible with Runnable
.
For the first case, the compiler has to choose between the two methods because both are potentially applicable. It does so using the procedure called 'Choose the Most Specific Method' and described in $15.12.2.5. Here is an excerpt:
A functional interface type S is more specific than a functional interface type T for an expression e if T is not a subtype of S and one of the following is true (where U1 ... Uk and R1 are the parameter types and return type of the function type of the capture of S, and V1 ... Vk and R2 are the parameter types and return type of the function type of T):
If e is an explicitly typed lambda expression (§15.27.1), then one of the following is true:
R2 is void.
First of all,
A lambda expression with zero parameters is explicitly typed.
Also, neither of Runnable
and Callable
is a subclass of one another, and Runnable
return type is void
, so we have a match: Callable
is more specific than Runnable
. This means that between submit(Callable)
and submit(Runnable)
in the first case the method with Callable
will be chosen.
As for the second case, there we only have one potentially applicable method, submit(Runnable)
, so it is chosen.
So, in the end, we can see that in these cases different methods are chosen by the compiler. In the first case, the lambda is inferred to be a Callable
which has throws Exception
on its call()
method, so that sleep()
call compiles. In the second case, it's Runnable
which run()
does not declare any throwable exceptions, so the compiler complains about an exception not being caught.