Why is
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {...}
more strict then
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {...}
This is a follow up on Why is lambda return type not checked at compile time.
I found using the method withX()
like
.withX(MyInterface::getLength, "I am not a Long")
produces the wanted compile time error:
The type of getLength() from the type BuilderExample.MyInterface is long, this is incompatible with the descriptor's return type: String
while using the method with()
does not.
import java.util.function.Function;
public class SO58376589 {
public static class Builder<T> {
public <R, F extends Function<T, R>> Builder<T> withX(F getter, R returnValue) {
return this;
}
public <R> Builder<T> with(Function<T, R> getter, R returnValue) {
return this;
}
}
static interface MyInterface {
public Long getLength();
}
public static void main(String[] args) {
Builder<MyInterface> b = new Builder<MyInterface>();
Function<MyInterface, Long> getter = MyInterface::getLength;
b.with(getter, 2L);
b.with(MyInterface::getLength, 2L);
b.withX(getter, 2L);
b.withX(MyInterface::getLength, 2L);
b.with(getter, "No NUMBER"); // error
b.with(MyInterface::getLength, "No NUMBER"); // NO ERROR !!
b.withX(getter, "No NUMBER"); // error
b.withX(MyInterface::getLength, "No NUMBER"); // error !!!
}
}
javac SO58376589.java
SO58376589.java:32: error: method with in class Builder<T> cannot be applied to given types;
b.with(getter, "No NUMBER"); // error
^
required: Function<MyInterface,R>,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where R,T are type-variables:
R extends Object declared in method <R>with(Function<T,R>,R)
T extends Object declared in class Builder
SO58376589.java:34: error: method withX in class Builder<T> cannot be applied to given types;
b.withX(getter, "No NUMBER"); // error
^
required: F,R
found: Function<MyInterface,Long>,String
reason: inference variable R has incompatible bounds
equality constraints: Long
lower bounds: String
where F,R,T are type-variables:
F extends Function<MyInterface,R> declared in method <R,F>withX(F,R)
R extends Object declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
SO58376589.java:35: error: incompatible types: cannot infer type-variable(s) R,F
b.withX(MyInterface::getLength, "No NUMBER"); // error
^
(argument mismatch; bad return type in method reference
Long cannot be converted to String)
where R,F,T are type-variables:
R extends Object declared in method <R,F>withX(F,R)
F extends Function<T,R> declared in method <R,F>withX(F,R)
T extends Object declared in class Builder
3 errors
The following example shows the different behaviour of method and type parameter boiled down to a Supplier. In addition it shows the difference to a Consumer behaviour for a type parameter. And it shows it does not make a difference wether it is a Consumer or Supplier for a method parameter.
import java.util.function.Consumer;
import java.util.function.Supplier;
interface TypeInference {
Number getNumber();
void setNumber(Number n);
@FunctionalInterface
interface Method<R> {
TypeInference be(R r);
}
//Supplier:
<R> R letBe(Supplier<R> supplier, R value);
<R, F extends Supplier<R>> R letBeX(F supplier, R value);
<R> Method<R> let(Supplier<R> supplier); // return (x) -> this;
//Consumer:
<R> R lettBe(Consumer<R> supplier, R value);
<R, F extends Consumer<R>> R lettBeX(F supplier, R value);
<R> Method<R> lett(Consumer<R> consumer);
public static void main(TypeInference t) {
t.letBe(t::getNumber, (Number) 2); // Compiles :-)
t.lettBe(t::setNumber, (Number) 2); // Compiles :-)
t.letBe(t::getNumber, 2); // Compiles :-)
t.lettBe(t::setNumber, 2); // Compiles :-)
t.letBe(t::getNumber, "NaN"); // !!!! Compiles :-(
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, (Number) 2); // Compiles :-)
t.lettBeX(t::setNumber, (Number) 2); // Compiles :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
t.letBeX(t::getNumber, "NaN"); // Does not compile :-)
t.lettBeX(t::setNumber, "NaN"); // Does not compile :-)
t.let(t::getNumber).be(2); // Compiles :-)
t.lett(t::setNumber).be(2); // Compiles :-)
t.let(t::getNumber).be("NaN"); // Does not compile :-)
t.lett(t::setNumber).be("NaN"); // Does not compile :-)
}
}
A type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.
It is called generics.
This is a really interesting question. The answer, I'm afraid, is complicated.
Working out the difference involves some quite in-depth reading of Java's type inference specification, but basically boils down to this:
with
there is a (admittedly vague) substitution that satisfies all the requirements on R
: Serializable
withX
, the introduction of the additional type parameter F
forces the compiler to resolve R
first, without considering the constraint F extends Function<T,R>
. R
resolves to the (much more specific) String
which then means that inference of F
fails.This last bullet point is the most important, but also the most hand-wavy. I can't think of a better concise way of phrasing it, so if you want more details, I suggest you read the full explanation below.
I'm gonna go out on a limb here, and say no.
I'm not suggesting there's a bug in the spec, more that (in the case of withX
) the language designers have put their hands up and said "there are some situations where type inference gets too hard, so we'll just fail". Even though the compiler's behaviour with respect to withX
seems to be what you want, I would consider that to be an incidental side-effect of the current spec, rather than a positively intended design decision.
This matters, because it informs the question Should I rely on this behaviour in my application design? I would argue that you shouldn't, because you can't guarantee that future versions of the language will continue to behave this way.
While it's true that language designers try very hard not to break existing applications when they update their spec/design/compiler, the problem is that the behaviour you want to rely on is one where the compiler currently fails (i.e. not an existing application). Langauge updates turn non-compiling code into compiling code all the time. For example, the following code could be guaranteed not to compile in Java 7, but would compile in Java 8:
static Runnable x = () -> System.out.println();
Your use-case is no different.
Another reason I'd be cautious about using your withX
method is the F
parameter itself. Generally, a generic type parameter on a method (that doesn't appear in the return type) exists to bind the types of multiple parts of the signature together. It's saying:
I don't care what T
is, but want to be sure that wherever I use T
it's the same type.
Logically, then, we would expect each type parameter to appear at least twice in a method signature, otherwise "it's not doing anything". F
in your withX
only appears once in the signature, which suggests to me a use of a type parameter not inline with the intent of this feature of the language.
One way to implement this in a slightly more "intended behaviour" way would be to split your with
method up into a chain of 2:
public class Builder<T> {
public final class With<R> {
private final Function<T,R> method;
private With(Function<T,R> method) {
this.method = method;
}
public Builder<T> of(R value) {
// TODO: Body of your old 'with' method goes here
return Builder.this;
}
}
public <R> With<R> with(Function<T,R> method) {
return new With<>(method);
}
}
This can then be used as follows:
b.with(MyInterface::getLong).of(1L); // Compiles
b.with(MyInterface::getLong).of("Not a long"); // Compiler error
This doesn't include an extraneous type parameter like your withX
does. By breaking down the method into two signatures, it also better expresses the intent of what you're trying to do, from a type-safety point of view:
With
) that defines the type based on the method reference.of
) constrains the type of the value
to be compatible with what you previously set up.The only way a future version of the language would be able to compile this is if the implemented full duck-typing, which seems unlikely.
One final note to make this whole thing irrelevant: I think Mockito (and in particular its stubbing functionality) might basically already do what you're trying to achieve with your "type safe generic builder". Maybe you could just use that instead?
I'm going to work through the type inference procedure for both with
and withX
. This is quite long, so take it slowly. Despite being long, I've still left quite a lot of details out. You may wish to refer to the spec for more details (follow the links) to convince yourself that I'm right (I may well have made a mistake).
Also, to simplify things a little, I'm going to use a more minimal code sample. The main difference is that it swaps out Function
for Supplier
, so there are less types and parameters in play. Here's a full snippet that reproduces the behaviour you described:
public class TypeInference {
static long getLong() { return 1L; }
static <R> void with(Supplier<R> supplier, R value) {}
static <R, F extends Supplier<R>> void withX(F supplier, R value) {}
public static void main(String[] args) {
with(TypeInference::getLong, "Not a long"); // Compiles
withX(TypeInference::getLong, "Also not a long"); // Does not compile
}
}
Let's work through the type applicability inference and type inference procedure for each method invocation in turn:
with
We have:
with(TypeInference::getLong, "Not a long");
The initial bound set, B0, is:
R <: Object
All parameter expressions are pertinent to applicability.
Hence, the initial constraint set for applicability inference, C, is:
TypeInference::getLong
is compatible with Supplier<R>
"Not a long"
is compatible with R
This reduces to bound set B2 of:
R <: Object
(from B0)Long <: R
(from the first constraint)String <: R
(from the second constraint)Since this does not contain the bound 'false', and (I assume) resolution of R
succeeds (giving Serializable
), then the invocation is applicable.
So, we move on to invocation type inference.
The new constraint set, C, with associated input and output variables, is:
TypeInference::getLong
is compatible with Supplier<R>
R
This contains no interdependencies between input and output variables, so can be reduced in a single step, and the final bound set, B4, is the same as B2. Hence, resolution succeeds as before, and the compiler breathes a sigh of relief!
withX
We have:
withX(TypeInference::getLong, "Also not a long");
The initial bound set, B0, is:
R <: Object
F <: Supplier<R>
Only the second parameter expression is pertinent to applicability. The first one (TypeInference::getLong
) is not, because it meets the following condition:
If
m
is a generic method and the method invocation does not provide explicit type arguments, an explicitly typed lambda expression or an exact method reference expression for which the corresponding target type (as derived from the signature ofm
) is a type parameter ofm
.
Hence, the initial constraint set for applicability inference, C, is:
"Also not a long"
is compatible with R
This reduces to bound set B2 of:
R <: Object
(from B0)F <: Supplier<R>
(from B0)String <: R
(from the constraint)Again, since this does not contain the bound 'false', and resolution of R
succeeds (giving String
), then the invocation is applicable.
Invocation type inference once more...
This time, the new constraint set, C, with associated input and output variables, is:
TypeInference::getLong
is compatible with F
F
Again, we have no interdependencies between input and output variables. However this time, there is an input variable (F
), so we must resolve this before attempting reduction. So, we start with our bound set B2.
We determine a subset V
as follows:
Given a set of inference variables to resolve, let
V
be the union of this set and all variables upon which the resolution of at least one variable in this set depends.
By the second bound in B2, the resolution of F
depends on R
, so V := {F, R}
.
We pick a subset of V
according to the rule:
let
{ α1, ..., αn }
be a non-empty subset of uninstantiated variables inV
such that i) for alli (1 ≤ i ≤ n)
, ifαi
depends on the resolution of a variableβ
, then eitherβ
has an instantiation or there is somej
such thatβ = αj
; and ii) there exists no non-empty proper subset of{ α1, ..., αn }
with this property.
The only subset of V
that satisfies this property is {R}
.
Using the third bound (String <: R
) we instantiate R = String
and incorporate this into our bound set. R
is now resolved, and the second bound effectively becomes F <: Supplier<String>
.
Using the (revised) second bound, we instantiate F = Supplier<String>
. F
is now resolved.
Now that F
is resolved, we can proceed with reduction, using the new constraint:
TypeInference::getLong
is compatible with Supplier<String>
Long
is compatible with String
... and we get a compiler error!
The Extended Example in the question looks at a few interesting cases that aren't directly covered by the workings above:
Integer <: Number
)Consumer
rather than Supplier
)In particular, 3 of the given invocations stand out as potentially suggesting 'different' compiler behaviour to that described in the explanations:
t.lettBe(t::setNumber, "NaN"); // Does not compile :-)
t.letBeX(t::getNumber, 2); // !!! Does not compile :-(
t.lettBeX(t::setNumber, 2); // Compiles :-)
The second of these 3 will go through exactly the same inference process as withX
above (just replace Long
with Number
and String
with Integer
). This illustrates yet another reason why you shouldn't rely on this failed type inference behaviour for your class design, as the failure to compile here is likely not a desirable behaviour.
For the other 2 (and indeed any of the other invocations involving a Consumer
you wish to work through), the behaviour should be apparent if you work through the type inference procedure laid out for one of the methods above (i.e. with
for the first, withX
for the third). There's just one small change you need to take note of:
t::setNumber
is compatible with Consumer<R>
) will reduce to R <: Number
instead of Number <: R
as it does for Supplier<R>
. This is described in the linked documentation on reduction.I leave it as an excercise for the reader to carfully work through one of the above procedures, armed with this piece of additional knowledge, to demonstrate to themselves exactly why a particular invocation does or doesn't compile.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With