Why does a lambda change overloads when it throws a runtime exception?

  • A+
Category:Languages

Bear with me, the introduction is a bit long-winded but this is an interesting puzzle.

I have this code:

public class Testcase {     public static void main(String[] args)     {         EventQueue queue = new EventQueue();         queue.add(() -> System.out.println("case1"));         queue.add(() ->         {             System.out.println("case2");             throw new IllegalArgumentException("case2-exception");         });         queue.runNextTask();         queue.add(() -> System.out.println("case3-never-runs"));     }      private static class EventQueue     {         private final Queue<Supplier<CompletionStage<Void>>> queue = new ConcurrentLinkedQueue<>();          public void add(Runnable task)         {             queue.add(() -> CompletableFuture.runAsync(task));         }          public void add(Supplier<CompletionStage<Void>> task)         {             queue.add(task);         }          public void runNextTask()         {             Supplier<CompletionStage<Void>> task = queue.poll();             if (task == null)                 return;             try             {                 task.get().                     whenCompleteAsync((value, exception) -> runNextTask()).                     exceptionally(exception ->                     {                         exception.printStackTrace();                         return null;                     });             }             catch (Throwable exception)             {                 System.err.println("This should never happen...");                 exception.printStackTrace();             }         }     } } 

I am trying to add tasks onto a queue and run them in order. I was expecting all 3 cases to invoke the add(Runnable) method; however, what actually happens is that case 2 gets interpreted as a Supplier<CompletionStage<Void>> that throws an exception before returning a CompletionStage so the "this should never happen" code block gets triggered and case 3 never runs.

I confirmed that case 2 is invoking the wrong method by stepping through the code using a debugger.

Why isn't the Runnable method getting invoked for the second case?

Apparently this issue only occurs on Java 10 or higher, so be sure to test under this environment.

UPDATE: According to JLS §15.12.2.1. Identify Potentially Applicable Methods and more specifically JLS §15.27.2. Lambda Body it seems that () -> { throw new RuntimeException(); } falls under the category of both "void-compatible" and "value-compatible". So clearly there is some ambiguity in this case but I certainly don't understand why Supplier is any more appropriate of an overload than Runnable here. It's not as if the former throws any exceptions that the latter does not.

I don't understand enough about the specification to say what should happen in this case.

 


It appears that when throwing an Exception, the compiler chooses the interface which returns a reference.

interface Calls {     void add(Runnable run);      void add(IntSupplier supplier); }  // Ambiguous call calls.add(() -> {         System.out.println("hi");         throw new IllegalArgumentException();     }); 

However

interface Calls {     void add(Runnable run);      void add(IntSupplier supplier);      void add(Supplier<Integer> supplier); } 

complains

Error:(24, 14) java: reference to add is ambiguous both method add(java.util.function.IntSupplier) in Main.Calls and method add(java.util.function.Supplier) in Main.Calls match

Lastly

interface Calls {     void add(Runnable run);      void add(Supplier<Integer> supplier); } 

compiles fine.

So weirdly;

  • void vs int is ambiguous
  • int vs Integer is ambiguous
  • void vs Integer is NOT ambiguous.

So I figure something is broken here.

I have sent a bug report to oracle.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: