36

Does anybody understand why the following code will compile fine in Java 7 and below, but fails with Java 8.

public static void main(String[] args) throws Exception {
    put(get("hello"));
}

public static <R> R get(String d) {
    return (R)d;
}

public static void put(Object o) {
    System.err.println("Object " + o);
}

public static void put(CharSequence c) {
    System.err.println("CharSequence " + c);
}

public static void put(char[] c) {
    System.err.println("char[] " + c);
}

The get method has a generic return type. In JDK 7 and below this compiles fine and the put method with the Object parameter is chosen. In JDK 8 this cannot be compiled, indicating the put method is ambiguous.

Apparently JDK 8 is skipping over the Object-parameter method and finding the last two sub-Object-parameter methods and complaining about them (i.e. if you add another put method with some other parameter type, the compiler will switch and complain about the new last two methods)

This seems like a bug.

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
Ken
  • 503
  • 1
  • 4
  • 10
  • Are you sure about this, *compiles fine with Java7 and not Java8* ? – akash Aug 30 '15 at 07:11
  • 8
    The `get` method is bogus. What do you expect `R` to be? Inferred from the value assigned to? There are 2 choices when call `put` directly, so which `R` should it use? Hiding an unverified cast behind a generic is really **bad**. – Andreas Aug 30 '15 at 07:12
  • This does **not** compile in Java 7. Sample code: https://ideone.com/gjzx8G – Tunaki Aug 30 '15 at 07:14
  • 2
    This code code compiles without errors on my IDE with Java 8 compiler. There is only a warning about type safety on the casting in return of the generic get method – Sharon Ben Asher Aug 30 '15 at 07:14
  • @sharonbn It fails when javac is used. – Amila Aug 30 '15 at 07:16
  • @sharonbn Which IDE are you using? This code fails for me with Eclipse Mars and Java 8. – Tunaki Aug 30 '15 at 07:17
  • @Amila. I don't understand. I though my IDE (Eclipse) invokes javac. in any case, a cimpiled class can also be executed. – Sharon Ben Asher Aug 30 '15 at 07:17
  • @Tunaki, Eclipse Luna SR2 – Sharon Ben Asher Aug 30 '15 at 07:19
  • @sharonbn Then I guess that Eclipse improved their compiler in Mars. – Tunaki Aug 30 '15 at 07:22
  • 3
    Why cast a `String` generically? `String` is `final` and directly extends `Object` so the only thing you could cast it to is `Object`. – Parker Hoyes Aug 30 '15 at 07:25
  • 3
    @Parker, but `String` implements 3 interfaces, so you can also cast it to any of the interfaces... – Codebender Aug 30 '15 at 07:43
  • @Tunaki This does get compiled with javac 7 in my command line. Perhaps ideone complies using JDK 8 with language level set to Java 7. – Amila Aug 30 '15 at 07:58
  • This was not what I was intimating. Take an untyped map, a map that can take anything as a value. When getting the values from that map, it would be nice for the compiler to infer the type of the value (e.g. String s = map.get("key"), or Integer i = map.get("key")). Now when using that returned value with a StringBuilder (e.g. sb.append(map.get("key"))), JDK 7 and below will choose append(Object), while JDK 8 will attempt to choose append(String). To me this is backwards. – Ken Sep 01 '15 at 23:10

2 Answers2

43

Your problem is a side-effect of Generalized Target-type Inference, an improvement in Java 8.

What is Target-type Inference

Let's take your example method,

public static <R> R get(String d) {
    return (R)d;
}

Now, in the method above, the generic parameter R cannot be resolved by the compiler because there's no parameter with R.

So, they introduced a concept called Target-type Inference, which allows the parameter to be inferred based on the assignment parameter.

So, if you do,

 String str = get("something"); // R is inferred as String here
 Number num = get("something"); // R is inferred as Number here

This works well in Java 7. But the following does not,

put(get("something");
static void Put(String str) {} //put method

Because type inference worked only for direct assignments.

If there's no direct assignment, then the generic type was inferred as Object.

So, when you compiled the code with Java 7, your put(Object) method was called without any problems.

What they did in Java 8

They improved the type inference to infer the type from method calls and chained method calls

More details about them here and here

So now, you can directly call put(get("something")) and the generic type will be inferred based on the parameter of the put() method.

But as you know, the methods, put(Charsequence) and put(char[]) match the arguments. So there's the ambiguity.

Fix?

Just tell the compiler exactly what you want,

put(TestClass.<CharSequence>get("hello")); // This will call the put(CharSequence) method.
Codebender
  • 14,221
  • 7
  • 48
  • 85
  • "chained method calls" - do you mean nested method calls? usually "chained" means something like "foo().bar()" – ZhongYu Aug 30 '15 at 16:49
  • there are 3 ways to provide target typing - assignment; method arg; casting. so we can solve it with a casting context too - `put( (CharSequence)get("hello") );` – ZhongYu Aug 30 '15 at 16:51
  • @bayou.io, I meant chained methods only... If you look at the 2nd link I have included, they have mentioned clearly why it's useful... – Codebender Aug 30 '15 at 17:21
  • 2
    yeah, it would be, but it is not included in java8 :) – ZhongYu Aug 30 '15 at 17:45
  • 3
    The correct fix is to change that nonsensical `get` method rather than tuning the type inference on it. As long as that method allows `put(TestClass.get("hello"));` as well, it’s broken. – Holger Aug 31 '15 at 09:06
  • 1
    @Ken, check [this SO](http://stackoverflow.com/questions/30521974/why-does-the-java-8-generic-type-inference-pick-this-overload) which explains why Object is not picked. In short, the **most specific** type is inferred. – Codebender Aug 31 '15 at 09:54
  • I have no problem with it choosing the most specific method, but in the event of a method ambiguity failure, shouldn't the compiler see if there is a less-specific method that can still satisfy the arguments. – Ken Sep 01 '15 at 23:48
2

Looks like this is a known incompatibility.

See "Area: Tools / javac" section of this article. And this bug.

Synopsis

The following code which compiled, with warnings, in JDK 7 will not compile in JDK 8:

import java.util.List;
 
class SampleClass {
 
    static class Baz<T> {
        public static List<Baz<Object>> sampleMethod(Baz<Object> param) {
            return null;
        }
    }
 
    private static void bar(Baz arg) {
        Baz element = Baz.sampleMethod(arg).get(0);
    }
} 

Compiling this code in JDK 8 produces the following error:

SampleClass.java:12: error:incompatible types: Object cannot be converted to Baz
    Baz element = Baz.sampleMethod(arg).get(0);
                                          
Note: SampleClass.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
1 error 

In this example, a raw type is being passed to the sampleMethod(Baz<Object>) method which is applicable by subtyping (see the JLS, Java SE 7 Edition, section 15.12.2.2).

An unchecked conversion is necessary for the method to be applicable, so its return type is erased (see the JLS, Java SE 7 Edition, section 15.12.2.6). In this case the return type of sampleMethod(Baz<Object>) is java.util.List instead of java.util.List<Baz<Object>> and thus the return type of get(int) is Object, which is not assignment-compatible with Baz.

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
Amila
  • 5,195
  • 1
  • 27
  • 46
  • hmm? this does not seem like an "incompatibility"; it is a bug in javac7, and it is fixed in javac8. – ZhongYu Aug 30 '15 at 16:45