Search code examples
javaeclipsegenericslambdaeclipse-neon

Type inference weirdness in Eclipse Neon


Good morning everybody,

I've struggled for some time understanding why the following piece of code doesn't compile in Eclipse Neon (JDT 3.12.3.v20170301-0400) but compiles perfectly with javac or Eclipse Mars:

public class TestLambda {

    protected static <K, V> Map<K, V> newMap(final Function<K, V> loader) {
        return new HashMap<>();
    }

    private final Map<Integer, Integer> working = newMap(key -> {

        final List<String> strings = new ArrayList<>();

        final String[] array = strings.toArray(new String[strings.size()]);
        foo(array);

        return null;
    });

    private final Map<Void, Void> notWorking = newMap(key -> {

        final List<String> strings = new ArrayList<>();

        // This line seems to be the root of all evils
        foo(strings.toArray(new String[strings.size()]));

        return null;
    });

    private void foo(final String[] x) {}

    private void foo(final Integer[] x) {}

}

The Eclipse compiler says "Type mismatch: cannot convert from Map<Object,Object> to Map<Void,Void>".

It seems that it cannot know which foo method must be called...

Do I do something wrong?

Thanks in advance for your help


Solution

  • I can reproduce the problem even with the newest version, Eclipse Oxygen. The problem does not occur with any javac version of Java 8 or Java 9 beta.

    We can clearly conclude that this is a bug and a weird one.

    First, there is no ambiguity here. Both invocations of foo have to end up at foo(String[]) and, in fact, Eclipse doesn’t report an ambiguity problem.

    Second, the foo invocation has no impact on the functional type of this lambda expression.

    If we change the line foo(strings.toArray(new String[strings.size()])); to foo(strings.toArray());, we will get a real error at foo as no foo(Object[]) exist, but the type inference will correctly resolve Function<Void, Void> for the lambda expression and Map<Void, Void> as newMap’s return type.

    We could also create a real ambiguity problem by changing the foo declarations to

    private void foo(final Serializable[] x) {}
    private void foo(final CharSequence[] x) {}
    

    This will cause an error on both foo invocations, as String[] is compatible with both methods and there is no relationship between Serializable and CharSequence, but the type inference for the newMap invocation is unaffected and resolves to
    <Void, Void> Map<Void, Void> newMap(Function<Void, Void>) as expected.

    The weirdest thing is, just swapping the order of the foo methods makes the error go away:

    public class TestLambda {
    
        protected static <K, V> Map<K, V> newMap(final Function<K, V> loader) {
            return new HashMap<>();
        }
    
        private final Map<Integer, Integer> working = newMap(key -> {
    
            final List<String> strings = new ArrayList<>();
    
            final String[] array = strings.toArray(new String[strings.size()]);
            foo(array);
    
            return null;
        });
    
        private final Map<Void, Void> notWorking = newMap(key -> {
    
            final List<String> strings = new ArrayList<>();
    
            // This line seems to be the root of all evils
            foo(strings.toArray(new String[strings.size()]));
    
            return null;
        });
    
        private void foo(final Integer[] x) {}
        private void foo(final String[] x) {}
    }
    

    compiles fine.

    There is a pattern behind it. You can have as many foo overloads you want, as long as the last declared one is applicable to the foo invocation. It doesn’t have to be the most specific one that is finally invoked, e.g. placing another private void foo(final Object[] x) {} at the end of the class will also solve the issue.

    That pattern surely has no matching Java language rule…