Reputation: 1511
Suppose that I want to write a method buildBiFxnWithSameTypeArgs
, which produces a two-argument BiFunction
whose input arguments are of the same type. It is not known ahead of time (i.e. when buildBiFxnWithSameTypeArgs
is invoked) specifically what type that is -- I just want the compiler to enforce that whenever the function returned by buildBiFxnWithSameTypeArgs
is invoked, the types of the arguments provided to it must match. The end effect should be, essentially, the lambda equivalent of using the same generic type parameter for the type of two arguments in a method definition.
My initial attempt looks like the following:
public interface ConstrainedBiFunction<I, O> extends BiFunction<I, I, O> {}
public static ConstrainedBiFunction<?, String> buildBiFxnWithSameTypeArgs() {
return (inputOne, inputTwo) -> String.valueOf(inputOne) + String.valueOf(inputTwo);
}
public void test() {
buildBiFxnWithSameTypeArgs().apply(Integer.valueOf(1), Integer.valueOf(2));
}
It appears, however, that the wildcard type parameter ?
cannot be used in this way; compilation fails on the apply
step with the following error:
apply (capture<?>, capture<?> in BiFunction cannot be applied to (java.lang.Integer, java.lang.Integer)
Is it possible to return a lambda function whose parameter types are inferred, then constrained relative to one another in this way?
EDIT: Apologies for the not-very-good question; I had a more complex problem of which I was trying to capture the essence in order to ask the simplest-possible question here, but it looks like I simplified the problem too much. I'm aware, of course, that all classes inherit from Object
, so in this over-simplified rendition of the problem, the solutions proposed by @shmosel and @JB Nizet work. I'll post a new question once I've done a better job distilling the original problem.
Upvotes: 0
Views: 1127
Reputation: 298143
While shmosel’s answer correctly points out that using Object
as input type should be sufficient for all APIs following the PECS rule, it is possible to fix your original attempt.
Instead of a wildcard, you have to add a type parameter which allows the caller to choose a type:
public interface ConstrainedBiFunction<I, O> extends BiFunction<I, I, O> {}
public static <T> ConstrainedBiFunction<T, String> buildBiFxnWithSameTypeArgs() {
return (inputOne, inputTwo) -> String.valueOf(inputOne) + String.valueOf(inputTwo);
}
public void test() {
ConstrainedBiFunction<Integer, String> f1 = buildBiFxnWithSameTypeArgs();
String s1 = f1.apply(1, 1);
ConstrainedBiFunction<String, String> f2 = buildBiFxnWithSameTypeArgs();
String s2 = f2.apply("hello ", "world");
}
Note that when using it as buildBiFxnWithSameTypeArgs().apply(1, 1);
, the compiler will infer Object
for T
anyway. This allow to pass Integer
arguments, but, of course, also anything else, which makes the idea of having the same type for both arguments moot, i.e. buildBiFxnWithSameTypeArgs().apply(1.5, "foo");
works as well, as both arguments are assignable to the same type Object
.
There are some real life examples for such constructs dealing with problems not solvable via PECS.
E.g. there is Function.identity()
returning an arbitrary typed function having the same input and output type. When passing this function to the toMap
collector, you can get a map having the same key or value type as the stream elements.
A more complex example is Comparator.naturalOrder()
. Here, the type parameter has a bound that ensures that the elements to compare are indeed Comparable
. Since the types don’t implement Comparable<?>
, but rather a type referring to itself, the naturalOrder()
wouldn’t work without the type parameter.
So you can’t do Comparator.naturalOrder().compare(42, "foo")
. But you can do
Comparator<Integer> c1 = Comparator.naturalOrder();
c1.compare(10, 42);
Comparator<String> c2 = Comparator.naturalOrder();
c2.compare("foo", "bar");
Upvotes: 0
Reputation: 50716
The wildcard means "of a specific, but unknown type". You can't pass any argument that will satisfy a parameter of type ?
, since it may be of the wrong type. Change the return type to ConstrainedBiFunction<Object, String>
and it will be able to accept any input type, since every class implicitly extends Object
:
public static ConstrainedBiFunction<Object, String> buildBiFxnWithSameTypeArgs() {
return (inputOne, inputTwo) -> String.valueOf(inputOne) + String.valueOf(inputTwo);
}
Note that this can still be used for methods with restrictions on the input type, using the PECS principle. For example:
// input parameter must be of type Integer or any supertype,
// so that we can safely pass in an Integer
String combine(ConstrainedBiFunction<? super Integer, String> function) {
return function.apply(1, 2);
}
void test() {
ConstrainedBiFunction<Object, String> concat = buildBiFxnWithSameTypeArgs();
ConstrainedBiFunction<Integer, String> sum = (a, b) -> String.valueOf(a + b);
System.out.println(combine(concat)); // "12"
System.out.println(combine(sum)); // "3"
}
Upvotes: 1