Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Differences in type inference JDK8 javac/Eclipse Luna?

I'm trying to switch a project to Java8, and encounter odd differences between Eclipse Luna and javac's type inference. With JDK 1.7.0_65 javac this code compiles just fine. JDK 1.8.0_11 complains that both toString(char[]) and toString(Throwable) match for the "toString(getKey(code, null));" line. Eclipse Luna 4.4 (I20140606-1215) compiles it happily with either JDK:

public class TypeInferenceTest {
    public static String toString(Object obj) {
        return "";
    }

    public static String toString(char[] ca) {
        return "";
    }

    public static String toString(Throwable t) {
        return "";
    }

    public static <U> U getKey(Object code, U defaultValue) {
        return defaultValue;
    }

    public static void test() {
        Object code = "test";
        toString(getKey(code, null));
    }
}

I think the only signature that could possibly match is toString(Object).

Of course I could simply add a cast to Object, but I wonder why javac can't infere the type by itself (while eclipse does), and why the heck javac considers Throwable and char[] suitable matches, but not Object.

Is this a bug in Eclipse or javac? (I mean only one compiler can be right here, either it compiles or it doesn't)

Edit: Error message from javac (JDK8):

C:\XXXX\Workspace\XXXX\src>javac -cp . TypeInferenceTest.java
TypeInferenceTest.java:22: error: reference to toString is ambiguous
                toString(getKey(code, null));
                ^
  both method toString(char[]) in TypeInferenceTest and method toString(Throwable) in TypeInferenceTest match
1 error
like image 565
Durandal Avatar asked Jul 21 '14 14:07

Durandal


2 Answers

Compilers can only inspect the method signatures, not the method body, so that part is irrelevant.

This "reduces" your code to (psuedocode):

public class TypeInferenceTest {
    public static String toString(Object obj);

    public static String toString(char[] ca);

    public static String toString(Throwable t);

    public static <U> U getKey(Object code, U defaultValue);

    public static void test() {
        Object code = "test";
        toString(getKey(code, null));
    }
}

Also note that the <U> U getKey(...) really is: <U extends Object> U getKey(...).

All it knows that getKey(code, null) returns is: ? extends Object, so it returns a subtype of Object, or an Object itself.
There are three signatures that match, namely Object, char[] and Throwable, where both char[] and Throwable match equally and better than Object, because you asked for an ? extends Object.

So it cannot choose which is the correct one, because all three match the signature.

When you change it to:

public static Object getKey(Object code, Object defaultValue);

then only public static String toString(Object obj); matches, because it matches better as any other ? extends Object that is not equal to Object.

Edit, I looked over the original intent of the question: Why does it compile in Java 7, but not in Java 8?

In Java 8 type inference got greatly improved.

Whereas in Java 7 it could for example only infer that getKey returned an Object, it now in Java 8 infers that it returns an ? extends Object.

When using Java 7 there was only one match, namely Object.

To have the change visualized even better, consider this piece of code:

public class TypeInferenceTest {
    public static String toString(Object obj) { return "1"; }

    public static String toString(Throwable t) { return "2"; }

    public static <U> U getKey(Object code, U defaultValue) { return defaultValue; }

    public static void test() {
        Object code = "test";
        String result = toString(getKey(code, null));
        System.out.println(result);
    }

    public static void main(String[] args) {
        test();
    }
}

On Java 7 it prints 1, on Java 8 it prints 2, exactly because of the reasons I have outlined above.

like image 91
skiwi Avatar answered Sep 20 '22 11:09

skiwi


javac may actually be correct. The spec writes:

The null type has one value, the null reference, represented by the null literal null, which is formed from ASCII characters.

Therefore, the type of null is the null type.

The expression getKey(code, null) is a method invocation expression of a generic method. The spec defines its type as follows:

  • If the chosen method is generic and the method invocation does not provide explicit type arguments, the invocation type is inferred as specified in §18.5.2.

The actual description of the type inference algorithm is rather involved, but the type inferred for U must be assignable from the null type. Alas, this is true for all reference types, so which one to choose? The most logical one is the most specific such type, which is the null type. Therefore, the type of the method invocation expression probably is the null type.

Now, which method does the method invocation expression toString(getKey(code, null)) refer to? The spec writes:

The second step searches the type determined in the previous step for member methods. This step uses the name of the method and the argument expressions to locate methods that are both accessible and applicable, that is, declarations that can be correctly invoked on the given arguments.

There may be more than one such method, in which case the most specific one is chosen. The descriptor (signature plus return type) of the most specific method is the one used at run time to perform the method dispatch.

Since the type of the argument is the null type, all three toString methods are applicable. The spec writes:

A method is said to be maximally specific for a method invocation if it is accessible and applicable and there is no other method that is applicable and accessible that is strictly more specific.

If there is exactly one maximally specific method, then that method is in fact the most specific method; it is necessarily more specific than any other accessible method that is applicable. It is then subjected to some further compile-time checks as specified in §15.12.3.

It is possible that no method is the most specific, because there are two or more methods that are maximally specific. In this case:

  • If all the maximally specific methods have override-equivalent signatures (§8.4.2), then:

    • If exactly one of the maximally specific methods is concrete (that is, non-abstract or default), it is the most specific method.

    • Otherwise, if all the maximally specific methods are abstract or default, and the signatures of all of the maximally specific methods have the same erasure (§4.6), then the most specific method is chosen arbitrarily among the subset of the maximally specific methods that have the most specific return type.

      In this case, the most specific method is considered to be abstract. Also, the most specific method is considered to throw a checked exception if and only if that exception or its erasure is declared in the throws clauses of each of the maximally specific methods.

  • Otherwise, the method invocation is ambiguous, and a compile-time error occurs.

Both toString(char[]) and toString(Throwable) are more specific that toString(Object), but neither is more specific than the other, nor are their signatures override-equivalent.

Therefore, the method invocation is ambiguous, and rejected by the compiler.

like image 40
meriton Avatar answered Sep 19 '22 11:09

meriton