Reputation: 380
Is there a way to tell Java to NOT try to infer a type from a method reference that uses primitive types?
Here is a method that I wrote, the reason for this is irrelevant right now:
public static <F, T> Predicate<F> isEquals(
Function<F, T> aInMapFunction, T aInExpectedValue)
{
return aInActual -> Objects.equals(
aInMapFunction.apply(aInActual), aInExpectedValue);
}
Now, what if you pass a method reference to "isEquals" that returns a primitive type?
Predicate<String> lLengthIs20 = isEquals(String::length, 20);
This is all fine and dandy, except that Java will also accept this strange usage:
Predicate<String> lLengthIs20 = isEquals(String::length, "what the heck?!?!?");
This is because the compiler will infer type parameter T as "Serializable & Comparable<? extends Serializable & Comparable<?>>
", which will accept both Integer and String types.
This is undesirable, in my case, as I would like a compilation error rather than Java figuring out some crazy type argument. For my thing, I can also explicitly override method "isEquals" to take specific primitive types. For example:
public static <F> Predicate<F> isEquals(
ToIntFunction<F> aInMapFunction, int aInExpectedValue)
{
return aInActual ->
aInMapFunction.applyAsInt(aInActual) == aInExpectedValue;
}
This works fine, this method is invoked rather than the Object one when I pass in a method that returns a primitive int. The problem is that I still need the Object method, I cannot remove it, which will still cause the compiler to accept the weird invocation I listed above.
So the question is: is there a way for me to tell Java to not use the Object version of isEquals when the method reference returns a primitive type? I couldn't find anything, I'm feeling I'm out of luck in this one.
(NOTE: the actual implementation of the object version of isEquals works fine and should be safe. This is because Object.equals and Objects.equals accept Object parameters and a String object will never be equals to an Integer object. Semantically, however, this looks weird)
EDIT: after the comment from "paranoidAndroid", one idea that I just had is to wrap the method reference the following way:
public static <T> Function<T, Integer> wrap(ToIntFunction<T> aInFunction)
{
return aInFunction::applyAsInt;
}
And now,
Predicate<String> lLengthIs20 = isEquals(wrap(String::length), "what the heck?!?!?");
... generates a compilation error. Still not great though, maybe there is a better way. At least this is better than passing the type in explicitly, which kind of beats the purpose.
EDIT 2: I'm in Java 8 right now. Java 11 might behave differently here, I didn't test.
EDIT 3: I'm thinking there is nothing we can do here, this is just an implication of how type inference works in Java. Here is another example:
public static <T> boolean isEquals(T t1, T t2) {
return Objects.equals(t1, t2);
}
with this method, the following expression is perfectly valid:
System.out.println(isEquals(10, "20"));
This works because Java will try to resolve the type for T based on a common upper bound. It just happens that both Integer and String share the same upper bound Serializable & Comparable<? extends Serializable & Comparable<?>>
Upvotes: 8
Views: 746
Reputation: 22997
I think that this is not a bug, but a consequence of type inference. OP already mentioned it. The compiler will not try to match an exact type, but the most specific one.
Let us analyse how type inference works with the example provided by OP.
public static <F, T> Predicate<F> isEquals(Function<F, T> func, T expValue) {
return actual -> Objects.equals(func.apply(actual), expValue);
}
Predicate<String> lLengthIs20 = isEquals(String::length, "Whud?");
Here the target type is Predicate<String>
, and according to the return type of the method, which is Predicate<F>
(where F
is a generic type), F
is bound to a String
. Then the method reference String::length
is checked whether it fits into the method parameter Function<F, T>
, where F
is String
and T
some unbounded type. And this is important: while the method reference String::length
looks like its target type is Integer
, it is also compatible to Object
. Similarly, Object obj = "Hello".length()
is valid. It is not required to be an Integer
. Likewise, both Function<String, Object> func = String::length
and Function<String, Object> func = str -> str.length()
are valid and do not emit a compiler warning.
Inference is to defer the job of selecting the appropriate type to the compiler. You ask the compiler: "Please, could you fill in appropriate types, so that it'll work?" And then the compiler answers: "Okay, but I follow certain rules when selecting the type."
The compiler selects the most specific type. In the case of isEquals(String::length, 20)
, both the target type of String::length
and 20
is Integer
, so the compiler infers it as such.
However, in the case of isEquals(String::length, "Whud?")
the compiler first tries to infer T
to an Integer
because of the type of String::length
, but it fails to do so because of the type of the second argument. The compiler then tries to find the closest intersection of Integer
and String
.
Bypass? No, not really. Well, sometimes typecasting is a way of bypassing, like in the following example:
Object o = 23; // Runtime type is integer
String str = (String) o; // Will throw a ClassCastException
The typecast here is a potentially unsafe operation, because o
may or may not be a String
. With this typecast, you say to the compiler: "In this specific case, I know better than you" – with the risk of getting an exception during runtime.
Still, not all typecast operations are permitted:
Integer o = 23;
String str = (String) o;
// Results in a compiler error: "incompatible types: Integer cannot be converted to String"
But you can certainly aid the compiler.
One option may be to use a type witness:
Predicate<String> lLengthIs20 = YourClass.<String, Integer>isEquals(String::length, "what?");
This code will emit a compiler error:
incompatible types: String cannot be converted to Integer
Class<T>
parameter to isEquals
Another option would be to add a parameter to isEquals
:
public static <F, T> Predicate<F> isEquals(Class<T> type, Function<F, T> func, T expValue) {
return actual -> Objects.equals(func.apply(actual), expValue);
}
// This will succeed:
Predicate<String> lLengthIs20 = isEquals(Integer.class, String::length, 20);
// This will fail:
Predicate<String> lLengthIs20 = isEquals(Integer.class, String::length, "Whud?");
A third option may be typecasting. Here you cast String::length
to a Function<String, Integer>
, and now the compiler is restricted to F = String, T = Integer
. Now the usage of "Whud?"
causes trouble.
Predicate<String> predicate = isEquals((Function<String, Integer>) String::length, "Whud?");
Upvotes: 2
Reputation: 1964
„…is there a way for me to tell Java to not use the Object version…“
Yes. And the term — in the context of Generics — for telling Java to not use Object
is called: „Specifying a bound“.
My experiment confirmed that calling the following method as isEquals(String::hashCode, "What the theoretical fuck!&?*!?@!")
will produce error: no suitable method found for isEquals(String::hashCode,String)
...
public static <F extends String, T extends Number> Predicate<F> isEquals(Function<F, T> aFunction, T aValue)
{
return input -> Objects.equals(aFunction.apply(input), aValue);
}
If you have both, the above method, and the following one in the same class, then this version is called for isEquals(String::length, 20)
...
public static <F> Predicate<F> isEquals(ToIntFunction<F> aFunction, int aValue)
{
return input -> aFunction.applyAsInt(input) == aValue;
}
...But the first one is called for isEquals(String::length, Integer.valueOf(42))
.
Click the blue Execute button in this demo to see it working.
Upvotes: 0
Reputation: 583
As far as I'm concerned, this smells like a real java compiler bug to me..Compiler should be able to infer arguments without assignment to a variable, since we have Function<F, T> aInMapFunction
which should enforce T, as the compiler "knows" that String::length
returns an Integer.
However I came up with a sort of solution for you:
public class PredicateBuilder<F,T>
{
public Predicate<F> isEquals(
Function<F, T> aInMapFunction, T aInExpectedValue)
{
return aInActual -> Objects.equals(
aInMapFunction.apply(aInActual), aInExpectedValue);
}
}
and usage:
new PredicateBuilder<String, Integer>().isEquals(String::length, 5);
Won't compile with other argument types, won't compile either if you try this:
new PredicateBuilder<>().isEquals(String::length, 5);
Upvotes: 0