Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the Java 8 generic type inference pick this overload?

Consider the following program:

public class GenericTypeInference {      public static void main(String[] args) {         print(new SillyGenericWrapper().get());     }      private static void print(Object object) {         System.out.println("Object");     }      private static void print(String string) {         System.out.println("String");     }      public static class SillyGenericWrapper {         public <T> T get() {             return null;         }     } } 

It prints "String" under Java 8 and "Object" under Java 7.

I would have expected this to be an ambiguity in Java 8, because both overloaded methods match. Why does the compiler pick print(String) after JEP 101?

Justified or not, this breaks backward compatibility and the change cannot be detected at compile time. The code just sneakily behaves differently after upgrading to Java 8.

NOTE: The SillyGenericWrapper is named "silly" for a reason. I'm trying to understand why the compiler behaves the way it does, don't tell me that the silly wrapper is a bad design in the first place.

UPDATE: I've also tried to compile and run the example under Java 8 but using a Java 7 language level. The behavior was consistent with Java 7. That was expected, but I still felt the need to verify.

like image 477
Bogdan Calmac Avatar asked May 29 '15 05:05

Bogdan Calmac


People also ask

How can generic method be overloaded?

A generic method can also be overloaded by nongeneric methods. When the compiler encounters a method call, it searches for the method declaration that best matches the method name and the argument types specified in the call—an error occurs if two or more overloaded methods both could be considered best ...

What is type inference in Java 8?

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.

How does type Inference work in Java?

Type inference represents the Java compiler's ability to look at a method invocation and its corresponding declaration to check and determine the type argument(s). The inference algorithm checks the types of the arguments and, if available, assigned type is returned.

Can a generic class extend an interface?

Java Generic Classes and SubtypingWe can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses.


2 Answers

The rules of type inference have received a significant overhaul in Java 8; most notably target type inference has been much improved. So, whereas before Java 8 the method argument site did not receive any inference, defaulting to Object, in Java 8 the most specific applicable type is inferred, in this case String. The JLS for Java 8 introduced a new chapter Chapter 18. Type Inference that's missing in JLS for Java 7.

Earlier versions of JDK 1.8 (up until 1.8.0_25) had a bug related to overloaded methods resolution when the compiler successfully compiled code which according to JLS should have produced ambiguity error Why is this method overloading ambiguous? As Marco13 points out in the comments

This part of the JLS is probably the most complicated one

which explains the bugs in earlier versions of JDK 1.8 and also the compatibility issue that you see.


As shown in the example from the Java Tutoral (Type Inference)

Consider the following method:

void processStringList(List<String> stringList) {     // process stringList } 

Suppose you want to invoke the method processStringList with an empty list. In Java SE 7, the following statement does not compile:

processStringList(Collections.emptyList()); 

The Java SE 7 compiler generates an error message similar to the following:

List<Object> cannot be converted to List<String> 

The compiler requires a value for the type argument T so it starts with the value Object. Consequently, the invocation of Collections.emptyList returns a value of type List, which is incompatible with the method processStringList. Thus, in Java SE 7, you must specify the value of the value of the type argument as follows:

processStringList(Collections.<String>emptyList()); 

This is no longer necessary in Java SE 8. The notion of what is a target type has been expanded to include method arguments, such as the argument to the method processStringList. In this case, processStringList requires an argument of type List

Collections.emptyList() is a generic method similar to the get() method from the question. In Java 7 the print(String string) method is not even applicable to the method invocation thus it doesn't take part in the overload resolution process. Whereas in Java 8 both methods are applicable.

This incompatibility is worth mentioning in the Compatibility Guide for JDK 8.


You can check out my answer for a similar question related to overloaded methods resolution Method overload ambiguity with Java 8 ternary conditional and unboxed primitives

According to JLS 15.12.2.5 Choosing the Most Specific Method:

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

Then:

One applicable method m1 is more specific than another applicable method m2, for an invocation with argument expressions e1, ..., ek, if any of the following are true:

  1. m2 is generic, and m1 is inferred to be more specific than m2 for argument expressions e1, ..., ek by §18.5.4.

  2. m2 is not generic, and m1 and m2 are applicable by strict or loose invocation, and where m1 has formal parameter types S1, ..., Sn and m2 has formal parameter types T1, ..., Tn, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ n, n = k).

  3. m2 is not generic, and m1 and m2 are applicable by variable arity invocation, and where the first k variable arity parameter types of m1 are S1, ..., Sk and the first k variable arity parameter types of m2 are T1, ..., Tk, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ k). Additionally, if m2 has k+1 parameters, then the k+1'th variable arity parameter type of m1 is a subtype of the k+1'th variable arity parameter type of m2.

The above conditions are the only circumstances under which one method may be more specific than another.

A type S is more specific than a type T for any expression if S <: T (§4.10).

The second of three options matches our case. Since String is a subtype of Object (String <: Object) it is more specific. Thus the method itself is more specific. Following the JLS this method is also strictly more specific and most specific and is chosen by the compiler.

like image 176
medvedev1088 Avatar answered Oct 06 '22 18:10

medvedev1088


In java7, expressions are interpreted from bottom up (with very few exceptions); the meaning of a sub-expression is kind of "context free". For a method invocation, the types of the arguments are resolved fist; the compiler then uses that information to resolve the meaning of the invocation, for example, to pick a winner among applicable overloaded methods.

In java8, that philosophy does not work anymore, because we expect to use implicit lambda (like x->foo(x)) everywhere; the lambda parameter types are not specified and must be inferred from context. That means, for method invocations, sometimes the method parameter types decide the argument types.

Obviously there's a dilemma if the method is overloaded. Therefore in some cases, it's necessary to resolve method overloading first to pick one winner, before compiling the arguments.

That is a major shift; and some old code like yours will fall victim to incompatibility.

A workaround is to provide a "target typing" to the argument with "casting context"

    print( (Object)new SillyGenericWrapper().get() ); 

or like @Holger's suggestion, provide type parameter <Object>get() to avoid inference all together.


Java method overloading is extremely complicated; the benefit of the complexity is dubious. Remember, overloading is never a necessity - if they are different methods, you can give them different names.

like image 38
ZhongYu Avatar answered Oct 06 '22 19:10

ZhongYu