Briefly
ExecutorService
has both submit(Callable)
and submit(Runnable)
methods.
- In the first case (with the
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 it
- In the second case (with the
while (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.
The full story
Java Language Specification describes how the method is chosen during program compilation in $15.2.2 :
- Identify Potentially Applicable Methods ($15.12.2.1) which is done in 3 phases for strict, loose and variable arity invocation
- Choose the Most Specific Method ($15.12.2.5) from the methods found on the first step.
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).
Identify Potentially Applicable Methods
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
.
Choose the Most Specific Method
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 why does the change surface?
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.