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.