Inexact Method References
TIL that method references have complicated rules.
Todays topic is inspired by a Twitter post from Josh Bloch asking why the following Java code doesn’t compile:
void puzzler() {
ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);
}
First of all, I’ll take Josh’s word for it. But it looks to me like it could compile - System.out.println
invoked with no args will do some stdio and return null, so exec.submit()
can return a Future<Void>
. Hmmm.
Lets see the error then. I don’t usually role-play as a compiler when I have a good one to hand in jshell
:
jshell> ExecutorService exec = Executors.newCachedThreadPool();
exec ==> java.util.concurrent.ThreadPoolExecutor@56ef9176[ ... = 0, completed tasks = 0]
jshell> exec.submit(System.out::println);
| Error:
| reference to submit is ambiguous
| both method <T>submit(java.util.concurrent.Callable<T>) in java.util.concurrent.ExecutorService and method submit(java.lang.Runnable) in java.util.concurrent.ExecutorService match
| exec.submit(System.out::println);
| ^---------^
| Error:
| incompatible types: cannot infer type-variable(s) T
| (argument mismatch; bad return type in method reference
| void cannot be converted to T)
| exec.submit(System.out::println);
| ^------------------------------^
Two errors. The first is confusing - hence it being a puzzle ;-) Here it is again:
reference to submit is ambiguous
both <T>submit(java.util.concurrent.Callable<T>) ...
and submit(java.lang.Runnable) ... match
The compiler is complaining that it can’t tell whether method reference System.out::println
should have target type of Runnable
or Callable
.
But in other circumstances it can clearly tell that only Runnable
matches:
jshell> Runnable r = System.out::println // YEP!
r ==> $Lambda$17/267760927@25bbe1b6
jshell> Callable c = System.out::println // NOPE!
| Error:
| incompatible types: bad return type in method reference
| void cannot be converted to java.lang.Object
| Callable c = System.out::println;
| ^-----------------^
jshell> Callable<Void> c = System.out::println // ALSO NOPE!!
| Error:
| incompatible types: bad return type in method reference
| void cannot be converted to java.lang.Void
| Callable<Void> c = System.out::println;
| ^-----------------^
So that’s weird, isn’t it?
Fiddling about with this it seems to be triggered by System.out::println
being variadic. Here’s a minimal repro:
jshell> class Banana { void foo(){}; void foo(int s){} }
| created class Banana
jshell> exec.submit(new Banana()::foo)
(same error as before)
The Answer
As figured out by @lhochstein:
There is a concept of exactness which can be applied to a method reference. Quoting JLS 15.13.1:
For some method reference expressions, there is only one possible compile-time declaration with only one possible invocation type, regardless of the targeted function type. Such method reference expressions are said to be exact. A method reference expression that is not exact is said to be inexact.
Given this, it’s clear that System.out::println
is inexact as there are 10 overloads.
Elsewhere in the JLS we find that inexact method references are excluded from being considered in method overload resolution:
An argument expression is considered pertinent to applicability for a potentially applicable method m unless it has one of the following forms: […] An inexact method reference expression (§15.13.1).
Exactly…
Well, TIL that method references don’t work quite how I thought they did. I can see this as a simple case where it looks like it ought to work, but more complex examples are easy to imagine.
JDK8 improved a lot of things, but in Josh’s words “Compromises were required”. Here and here people have raised this behaviour as being a bug.