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.