Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type inference with method reference and primitive types

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<?>>

like image 926
Marcio Lucca Avatar asked Aug 12 '20 21:08

Marcio Lucca


People also ask

What is the type of inference?

Type inference refers to the automatic detection of the type of an expression in a formal language. These include programming languages and mathematical type systems, but also natural languages in some branches of computer science and linguistics.

What is type inference Java?

Type inference is a Java compiler's ability to look at each method invocation and corresponding declaration to determine the type argument (or arguments) that make the invocation applicable.

Which is a primitive type?

Primitive data types - includes byte , short , int , long , float , double , boolean and char.

Do primitive types have methods?

Primitives have no methods but still behave as if they do. When properties are accessed on primitives, JavaScript auto-boxes the value into a wrapper object and accesses the property on that object instead.


1 Answers

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.

What exactly is inference?

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.

Can I aid or bypass the compiler?

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.

Type witness

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

Add a 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?");

Typecasting

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?");
like image 175
MC Emperor Avatar answered Nov 04 '22 21:11

MC Emperor