Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can Java not infer a supertype?

We all know Long extends Number. So why does this not compile?

And how to define the method with so that the program compiles without any manual cast?

import java.util.function.Function;

public class Builder<T> {
  static public interface MyInterface {
    Number getNumber();
    Long getLong();
  }

  public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue) {
    return null;//TODO
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::getLong, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
    // works:
    new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);
    // works:
    new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, 4L);
    // compilation error: Cannot infer ...
    new Builder<MyInterface>().with(MyInterface::getNumber, Long.valueOf(4));
    // compiles but also involves typecast (and Casting Number to Long is not even safe):
    new Builder<MyInterface>().with( myInterface->(Long) myInterface.getNumber(), 4L);
    // compiles but also involves manual conversion:
    new Builder<MyInterface>().with(myInterface -> myInterface.getNumber().longValue(), 4L);
    // compiles (compiler you are kidding me?): 
    new Builder<MyInterface>().with(castToFunction(MyInterface::getNumber), 4L);

  }
  static <X, Y> Function<X, Y> castToFunction(Function<X, Y> f) {
    return f;
  }

}

  • Cannot infer type argument(s) for <F, R> with(F, R)
  • The type of getNumber() from the type Builder.MyInterface is Number, this is incompatible with the descriptor's return type: Long

For use case see: Why is lambda return type not checked at compile time

like image 822
jukzi Avatar asked Oct 14 '19 13:10

jukzi


2 Answers

This expression :

new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

can be rewritten as :

new Builder<MyInterface>().with(myInterface -> myInterface.getNumber(), 4L);

Taking into account method signature :

public <F extends Function<T, R>, R> Builder<T> with(F getter, R returnValue)
  • R will be inferred to Long
  • F will be Function<MyInterface, Long>

and you pass a method reference which will be infered as Function<MyInterface, Number> This is the key - how should compiler predict that you actually want to return Long from a function with such signature? It will not do the downcasting for you.

Since Number is superclass of Long and Number is not necessairly a Long (this is why it does not compile) - you would have to cast explicitly on your own :

new Builder<MyInterface>().with(myInterface -> (Long) myInterface.getNumber(), 4L);

making F to be Function<MyIinterface, Long> or pass generic arguments explicitly during method call as you did :

new Builder<MyInterface>().<Function<MyInterface, Number>, Number> with(MyInterface::getNumber, 4L);

and know R will be seen as Number and code will compile.

like image 102
Michał Krzywański Avatar answered Oct 11 '22 15:10

Michał Krzywański


The key to your error is in the generic declaration of the type of F: F extends Function<T, R>. The statement that does not work is: new Builder<MyInterface>().with(MyInterface::getNumber, 4L); First, you have a new Builder<MyInterface>. The declaration of the class therefore implies T = MyInterface. As per your declaration of with, F must be a Function<T, R>, which is a Function<MyInterface, R> in this situation. Therefore, the parameter getter must take a MyInterface as parameter (satisfied by the method references MyInterface::getNumber and MyInterface::getLong), and return R, which must be the same type as the second parameter to the function with. Now, let's see if this holds for all of your cases:

// T = MyInterface, F = Function<MyInterface, Long>, R = Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L explicitly widened to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().<Function<MyInterface, Number>, Number>with(MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Number
// 4L implicitly widened to Number
new Builder<MyInterface>().with((Function<MyInterface, Number>) MyInterface::getNumber, 4L);
// T = MyInterface, F = Function<MyInterface, Number>, R = Long
// F = Function<T, not R> violates definition, therefore compilation error occurs
// Compiler cannot infer type of method reference and 4L at the same time, 
// so it keeps the type of 4L as Long and attempts to infer a match for MyInterface::getNumber,
// only to find that the types don't match up
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

You can "fix" this problem with the following options:

// stick to Long
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// stick to Number
new Builder<MyInterface>().with(MyInterface::getNumber, (Number) 4L);
// explicitly convert the result of getNumber:
new Builder<MyInterface>().with(myInstance -> (Long) myInstance.getNumber(), 4L);
// explicitly convert the result of getLong:
new Builder<MyInterface>().with(myInterface -> (Number) myInterface.getLong(), (Number) 4L);

Beyond this point, it's mostly a design decision for which option reduces code complexity for your particular application, so choose whatever fits you best.

The reason that you cannot do this without casting lies in the following, from the Java Language Specification:

Boxing conversion treats expressions of a primitive type as expressions of a corresponding reference type. Specifically, the following nine conversions are called the boxing conversions:

  • From type boolean to type Boolean
  • From type byte to type Byte
  • From type short to type Short
  • From type char to type Character
  • From type int to type Integer
  • From type long to type Long
  • From type float to type Float
  • From type double to type Double
  • From the null type to the null type

As you can clearly see, there is no implicit boxing conversion from long to Number, and the widening conversion from Long to Number can only occur when the compiler is sure that it requires a Number and not a Long. As there is a conflict between the method reference that requires a Number and the 4L that provides a Long, the compiler (for some reason???) unable to make the logical leap that Long is-a Number and deduce that F is a Function<MyInterface, Number>.

Instead, I managed to resolve the problem by slightly editing the function signature:

public <R> Builder<T> with(Function<T, ? super R> getter, R returnValue) {
  return null;//TODO
}

After this change, the following occurs:

// doesn't work, as it should not work
new Builder<MyInterface>().with(MyInterface::getLong, (Number), 4L);
// works, as it always did
new Builder<MyInterface>().with(MyInterface::getLong, 4L);
// works, as it should work
new Builder<MyInterface>().with(MyInterface::getNumber, (Number)4L);
// works, as you wanted
new Builder<MyInterface>().with(MyInterface::getNumber, 4L);

Edit:
After spending some more time on it, it's annoyingly difficult to enforce getter-based type safety. Here's a working example that uses setter methods to enforce a builder's type-safety:

public class Builder<T> {

  static public interface MyInterface {
    //setters
    void number(Number number);
    void Long(Long Long);
    void string(String string);

    //getters
    Number number();
    Long Long();
    String string();
  }
  // whatever object we're building, let's say it's just a MyInterface for now...
  private T buildee = (T) new MyInterface() {
    private String string;
    private Long Long;
    private Number number;
    public void number(Number number)
    {
      this.number = number;
    }
    public void Long(Long Long)
    {
      this.Long = Long;
    }
    public void string(String string)
    {
      this.string = string;
    }
    public Number number()
    {
      return this.number;
    }
    public Long Long()
    {
      return this.Long;
    }
    public String string()
    {
      return this.string;
    }
  };

  public <R> Builder<T> with(BiConsumer<T, R> setter, R val)
  {
    setter.accept(this.buildee, val); // take the buildee, and set the appropriate value
    return this;
  }

  public static void main(String[] args) {
    // works:
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works:
    new Builder<MyInterface>().with(MyInterface::number, (Number) 4L);
    // compile time error, as it shouldn't work
    new Builder<MyInterface>().with(MyInterface::Long, (Number) 4L);
    // works, as it always did
    new Builder<MyInterface>().with(MyInterface::Long, 4L);
    // works, as it should
    new Builder<MyInterface>().with(MyInterface::number, (Number)4L);
    // works, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, 4L);
    // compile time error, as you wanted
    new Builder<MyInterface>().with(MyInterface::number, "blah");
  }
}

Provided the type-safe ability to construct an object, hopefully at some point in the future we'll be able to return an immutable data object from the builder (maybe by adding a toRecord() method to the interface, and specifying the builder as a Builder<IntermediaryInterfaceType, RecordType>), so you don't even have to worry about the resulting object being modified. Honestly, it's an absolute shame that it requires so much effort to get a type-safe field-flexible builder, but it's probably impossible without some new features, code generation, or an annoying amount of reflection.

like image 39
Avi Avatar answered Oct 11 '22 14:10

Avi