JDK-8208482 : javac infers type Supplier for lambda that throws
  • Type: Bug
  • Component: tools
  • Sub-Component: javac
  • Affected Version: 11
  • Priority: P4
  • Status: Closed
  • Resolution: Not an Issue
  • OS: windows_10
  • CPU: x86_64
  • Submitted: 2018-07-29
  • Updated: 2018-07-31
  • Resolved: 2018-07-31
Description
ADDITIONAL SYSTEM INFORMATION :
Tested on 64-bit Windows 10 build 17713 with jdk-11-ea+24, jdk-10.0.2, and jdk1.8u181.

A DESCRIPTION OF THE PROBLEM :
If there are two possible targets for a lambda (say, an overloaded method), one which is Runnable, and one which is a Supplier<?>, and the lambda does not return anything, but does return abnormally i.e. contains an unconditional throw statement, the compiler infers that the lambda is a Supplier even though a lambda which returns void can never be a Supplier.

The code won't actually produce a ClassCastException since calling get() on the Supplier will always throw, and any attempts I have made to alter the code to avoid throwing have caused it to switch back to compiling it to Runnable. However, I worry that I have not been clever enough, and there may be a way to get it to cause a ClassCastException at runtime after all.

Originally found by StackOverflow user "Gili" in this question: https://stackoverflow.com/questions/51577332/why-does-a-lambda-change-overloads-when-it-throws-a-runtime-exception#51577332

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Compile the attached test case, then examine the bytecode with javap -c Bug

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Both the call with the lambda that prints and the call with the lambda that throws should be Runnable.
ACTUAL -
The first call is with a Runnable, but the second call is with a Supplier<Integer>, even though the lambda cannot possibly be a Supplier<Integer>. The bytecode for the main method produced by javac from jdk-11 is as follows:

  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
       5: invokestatic  #3                  // Method method:(Ljava/lang/Runnable;)V
       8: invokedynamic #4,  0              // InvokeDynamic #1:get:()Ljava/util/function/Supplier;
      13: invokestatic  #5                  // Method method:(Ljava/util/function/Supplier;)V
      16: return

The first call is with Runnable, as expected. The second call, with Supplier, is erroneous.

---------- BEGIN SOURCE ----------
import java.util.function.Supplier;

public class Bug {
    public static void method(Runnable runnable) { }

    public static void method(Supplier<Integer> supplier) { }

    public static void main(String[] args) {
        method(() -> System.out.println());
        method(() -> { throw new RuntimeException(); });
    }
}

---------- END SOURCE ----------

FREQUENCY : always



Comments
Additional Information from submitter == I now believe my report is in error and the compiler is correct, due to §15.27.2 of the JLS which reads, in part, "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;." I would have thought that there must be at least one return statement with a value for the lambda to be value-compatible, however, per this rule, a lambda which cannot complete normally is trivially value-compatible if it contains zero return statements. Thus, the lambda is compatible with Supplier<Integer> and overload resolution proceeds, correctly finding the Supplier to be more specific than Runnable. If this is correct, as I now believe, then my report is in error and I apologize.
31-07-2018

This is not an issue according to https://stackoverflow.com/questions/51577332/why-does-a-lambda-change-overloads-when-it-throws-a-runtime-exception#51577332
31-07-2018