Java 8 Incompatible Types

2020-07-22 09:33发布

问题:

Here's the simple code

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class SimpleTest {

    public static void main(String[] args) {
    final ArrayList<Map<String, Object>> maps = newArrayList(
        createMap("1", "a", Collections.EMPTY_MAP, Collections.EMPTY_MAP),
        createMap("2", "b", Collections.EMPTY_MAP, Collections.EMPTY_MAP),
        createMap("3", "c", Collections.EMPTY_MAP, Collections.EMPTY_MAP)
    ); 

    System.out.println(" maps = " + maps);
    }

    public static Map<String, Object> createMap(String value1, String value2, Map<String, Object> object1, Map<String, Object> object2) {
       Map<String, Object> map = new HashMap<>();
       map.put("value1", value1);
       map.put("value1", value1);
       map.put("object1", object1);
       map.put("object2", object2);
       return map;
    }    

    public static <E> ArrayList<E> newArrayList(E... elements) {
    ArrayList<E> list = new ArrayList<E>(elements.length);
    Collections.addAll(list, elements);
    return list;
    }
}

When JAVA_HOME points to JDK 8 and I use javac -source 1.7 SimpleTest.java I get

SimpleTest.java:9: error: incompatible types: ArrayList<Map> cannot be converted to ArrayList<Map<String,Object>>
        final ArrayList<Map<String, Object>> maps = newArrayList(
                                                                ^

When I use -source 1.8 or no -source option everything works ok. Now, when JAVA_HOME points to JDK 7 the code always compiles whether I use -source 1.7 or not.

Initially this question was about a piece of software where POM file has a <source> and <target> set to 1.7 and the build was failing on JDK 8 but was ok on JDK 7.

Now for the question - what causes it to happen ? It seems to me as a major overlook of some sort. Why compiling on JDK 8 with source set to 1.7 fails ?

回答1:

You can simplify the code to a self-contained example which doesn’t need 3rd-party libraries:

public class Test2 {
    @SafeVarargs
    static <T> ArrayList<T> newArrayList(T... arg) {
        return new ArrayList<T>(Arrays.asList(arg));
    }
    private final List<Map<String, Object>> maps = newArrayList(
        createMap(null, Collections.EMPTY_MAP)
     );

    public static Map<String, Object> createMap(String id, Map<String,String> m) {
        return null;
    }
}

This can be compiled using javac from jdk1.7, but not with javac from jdk1.8 using -source 1.7.

The point here is that javac from jdk1.8 still is a different compiler than the one included in the previous version and the option -source 1.7 doesn’t tell it to mimic the old implementation’s behavior but to be compatible with the Java 7 specification. If the old compiler has a bug, the newer compiler doesn’t have to try to reproduce the bug.

Since the code uses Collections.EMPTY_MAP rather than Collections.<String,String>emptyMap(), the raw type Map will be passed to createMap, making it an unchecked invocation having the raw result type Map.

This is mandated by JLS §15.12.2.6:

The result type of the chosen method is determined as follows:

  • If the chosen method is declared with a return type of void, then the result is void.

  • Otherwise, if unchecked conversion was necessary for the method to be applicable, then the result type is the erasure (§4.6) of the method's declared return type.

It seems that this behavior has not been (fully) implemented in javac of jdk1.7. Interestingly, it will do it correctly when adding a type parameter like

public static <T> Map<String, Object> createMap(String id, Map<String,String> m)

making it a generic method. Then, jdk1.7 will produce the same error. But the cited rule applies to all method invocations.


In contrast, the fact that compiling it with Java 8 compliance succeeds stems from the new target type inference, which has entirely different rules.