8

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

6
  • I do not see why Predicate<String> lLengthIs20 = isEquals(String::length, "what the heck?!?!?") is strange behaviour. It's just the consequence of type inference. Commented Aug 13, 2020 at 16:07
  • 1
    Yes, I'm starting to agree with you, this is just how type inference works. After reading a bit the spec (not much though, the spec is crazy), you can see that inference tries to create bounds for different "inference variables" and then it proceeds to find the most common "proper types" that satisfy the given constraints, which results in this behaviour. Semantically I still find this a bit weird, but I can live with it. Commented Aug 13, 2020 at 17:32
  • Referring to your edit 3: but i don't see how it's related to Comparable. Even this compiles: isEquals(new Object(), "abc"); Commented Aug 14, 2020 at 10:08
  • Your last example doesn't make any sense lol. (I mean, this is really, really strange) Commented Aug 14, 2020 at 10:08
  • ...this is really, really strange...“ — @paranoidAndroid — Yeah, that's one way to put it. I call it: „Not obvious“ or „subtle“; maybe even „nuanced“ or „It's complicated“. That's my characterization of Java's type inference rules. And that's after having read and reread the JLS chapter on it 3 or 4 times at this point. To reduce the complexity of ch18 to be easily digestible/easily remembered, I abstract it in my mind to a simplified term: „Specifying a bound“. Commented Aug 15, 2020 at 9:39

3 Answers 3

2

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?"); 
Sign up to request clarification or add additional context in comments.

15 Comments

I still don't get it. If I pass a Function<String, Integer> (which is String::length) T is Integer, not some supertype of integer. T must conform to Integer. So the method parameter 'aInExpectedValue', declared T, must be Integer. What am I missing?
Furthermore, if we were to pass a Class<T> reference into the isEquals() method - everything works as expected. What is the difference between Class<T> and Function<F,T> ?
...What is the difference between Class<T> and Function<F,T> ? ...“ — 1 major difference, is that in Function<F,T>'s case the „method“ that 's created under the hood gets a receiver parameter that will be of type F. I learned about that just the other day from @Holger in a question about BiFunctions. Thanks again, Holger :) Not saying OP's thing can be attributed to that difference. But repeating new things I learned helps me remember them :)
Thanks MC Emperor. In the example you gave above you are bypassing type inference, which works, of course. I'm thinking this may be the only solution. Maybe in future versions of the language they may give programmers tools to help the compiler figure out the right types, but for now this doesn't seem to be the case. Type inference is a very complicated topic, the spec does talk about using simplifications at times to try and avoid making a complex problem even more difficult.
...In the example you gave above you are bypassing type inference...“ — @MarcioLucca? His answer hinges on the „type witness“ he refers to and shows in his example. My understanding is that a „type witness“ does the opposite of „bypassing type inference“. I always thought using a type witness aids type inference. Is that not the case?
|
0

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); 

3 Comments

Yes, this is one idea. Basically this is bypassing type inference, as you are providing the types in the function call, or in your case, when you create the PredicateBuilder.
This is not a compiler bug, but rather simply how inference works. The rules are slightly complicated, but they are defined very well. See Java Language Specification, chapter 18.
@MarcioLucca Yes, I think, after the long discussion, that this is the only way to enforce type correctness in your use case.
0

…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.

2 Comments

Thanks for the answer deduper. The problem is that I don't want to specify a bound to T, as T is supposed to be generic, that is, it may or may not be a Number. If I want to use for example Predicate<String> p = isEquals(Boolean::parseBoolean, true), then it wouldn't compile either, which is no good. Finally, I don't want to have to specify a new method every time I need to use a new type, as that would not be super generic.
BTW, I think that online IDE I pointed you at, has a bug. Does the MyClass.java editor window show up empty when you click on the link I shared?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.