7

While testing, I upgraded my Junit to 5.0 (Thus replacing some of my assertTrue() methods with the new versions). After doing so, I found one of my tests didn't compile. I reduced the issue down to plain old java with no junit or other dependencies. The result is the following code which will not compile:

  public static void recreate() {
    // This does NOT work
    Recreation.assertTrue(identity((x) -> Boolean.TRUE)); 
    // This DOES work
    Recreation.assertTrue(identity((String x) -> Boolean.TRUE)); 
  }

  private static class Recreation {
    public static void assertTrue(boolean b) {
      System.out.println("boolean argument: " + b);
    }

    // If this method is removed, the code will compile. 
    public static void assertTrue(Supplier<Boolean> booleanSupplier) {
      System.out.println("supplier argument: " + booleanSupplier.toString());
    }
  }

  private static <K> K identity(Function<String, K> function) {
    return function.apply("hello");
  }

As shown in the above example, the code will compile if either of the following are true:

  1. The lambda parameter type is specified

  2. The overloaded assertTrue(Supplier booleanSupplier) method is removed

Is this an issue with type inference/erasure, or what might be going on here?

Build Error:

Error:(10, 35) incompatible types: inference variable K has incompatible bounds
    lower bounds: java.util.function.Supplier<java.lang.Boolean>,java.lang.Object
    lower bounds: java.lang.Boolean

Specs:

openjdk version "11.0.1" 2018-10-16
OpenJDK Runtime Environment (build 11.0.1+13-Ubuntu-3ubuntu114.04ppa1)
OpenJDK 64-Bit Server VM (build 11.0.1+13-Ubuntu-3ubuntu114.04ppa1, mixed mode, sharing)

OS: Ubuntu 14.04.5 LTS

EDIT: Confirmed the issue exists on Java 8 as well:

java version "1.8.0_31"
Java(TM) SE Runtime Environment (build 1.8.0_31-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode)
exit status 1
Main.java:10: error: incompatible types: inferred type does not conform to upper bound(s)
    Recreation.assertTrue(identity((x) -> Boolean.TRUE));
                                  ^
    inferred: Boolean
    upper bound(s): Supplier<Boolean>,Object
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
user2770791
  • 553
  • 1
  • 5
  • 11

2 Answers2

4

After looking around and reading the Java Language Specification here https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12.2.1

I think there are two steps here:

First, the overloading resolution couldn't infer the type of identity((x) -> Boolean.TRUE) because it's implicit lambda, I think it's not taken into account for simplicity sake. Thus, it will widen the parameter search and use public static void assertTrue(Supplier<Boolean> booleanSupplier).

Second, after overloading resolution is done, type inference kicks in. This time it really check the inferred type which is a Boolean, and since it's not compatible with Supplier<Boolean> booleanSupplier, you get the compilation error.

Like previous answer, there are solutions to this,

e.g

Recreation.assertTrue(identity((x) -> () -> Boolean.TRUE));

I found a good explanation here: Java8: ambiguity with lambdas and overloaded methods

Bustanil Arifin
  • 396
  • 4
  • 4
  • Interesting, I hadn't realized that overloading resolution takes place before type inference. This would explain why I only saw the issue with lambdas, not anonymous classes or if I stored the lambda in a variable before calling identity. Thanks for your answer! – user2770791 Apr 04 '19 at 17:35
0

After extensive research, I stumbled upon this bug report. That report references this section in the Java specification, which gives you a basic knowledge on how all of this stuff works. Keep it in mind, because it will be necessary later.

The issue can be reduced to look like this (even though I know that's not what you want, it will reproduce the error):

Supplier<Boolean> o = identity((x) -> true);

So, I believe the issue resonates in how Java decides what types generics should be. When you specify Supplier<Boolean> as the type of o, it tells the compiler that the return type of identity should be Supplier<Boolean>.

Now, in your example, you don't have a variable that stores the output of identity. This is the part that comes from the spec. Whenever Java gets a generic type, that type needs to be within specific bounds. This basically means that there is a specific highest class and lowest class in the hierarchy of extension.

For example if you have class C that extends class B, which also extends class A, your upper bound could be C, and lower bound could be A. That way you could use anything from A to C. That is just an example of how bounds work, though.

This is basically what is happening in your class. Since you don't specify the String as the type for the parameter, Java doesn't know what type that could be. Since it doesn't know, it does its best to cast it to the correct type, but because generics are so ambiguous, it isn't able to cast it to the correct type. It somehow decides to use Supplier<Boolean> as the baseline(probably defined somewhere in the spec as well), and expects that as a return type. Since it doesn't get that, it throws an error. Why it doesn't decide to check if it is a Boolean is beyond me, but according to the bug report everything is working as intended.

Some potential fixes could look like:

Recreation.assertTrue(identity((Function<String, Boolean>) (x) -> Boolean.TRUE));
Recreation.assertTrue((Boolean) identity((x) -> Boolean.TRUE));

Or just implicitly telling it what type the parameter is.

Recreation.assertTrue(identity((String x) -> Boolean.TRUE));

I know I don't have the best understanding of this specific circumstance, but this should get you at least a baseline understanding of how the whole system works.

Fishy
  • 1,275
  • 1
  • 12
  • 26