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);
2.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;
2. 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:
- Detects that all execution paths in the lambda declare throwing checked exceptions (from now on we will refer as ‘exception’, implying only ‘checked exceptions’). This includes the invocation of any method declaring throwing exceptions and the explicit invocation to
throw new <CHECKED_EXCEPTION>(). - Concludes correctly that the WHOLE body of the lambda is equivalent to a block of code declaring throwing exceptions; which of course MUST be either: handled or re-thrown.
- Since the lambda is not handling the exception, then the compiler defaults to assume that these exception(s) must be re-thrown.
- Safely infers that this lambda must match a functional interface cannot
complete normallyand therefore is value-compatible. - Since
Callable<?>andRunnableare potential matches for this lambda, the compiler selects the most specific match (to cover all scenarios); which is theCallable<?>, converting the lambda into an instance of it and creating an invocation reference to thesubmit(Callable<?>)overloaded method.
While, in the second case, the compiler does the following:
- Detects that there may be execution paths in the lambda that DO NOT declare throwing exceptions (depending on to-be-evaluated logic).
- Since not all execution paths declare throwing exceptions, the compiler concludes that the body of the lambda is NOT NECESSARILY equivalent to a block of code declaring throwing exceptions – compiler doesn’t care/pay attention if some portions of the code do declare that they may, only if the whole body does or not.
- Safely infers that the lambda is not value-compatible; since it MAY
complete normally. - Selects
Runnable(as it is the only available fitting functional interface for the lambda to be converted into) and creates an invocation reference to thesubmit(Runnable)overloaded method. All this coming at the price of delegating to the user, the responsibility of handling anyExceptions 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!