Why does this code fail to compile, citing type inference as the cause?
Asked Answered
D

2

10

Here's a minimal example of the code I'm working with:

public class Temp {
    enum SomeEnum {}

    private static final Map<SomeEnum, String> TEST = new EnumMap<>(
               Arrays.stream(SomeEnum.values())
                     .collect(Collectors.toMap(t -> t, a -> "")));

}

The compiler output is:

Temp.java:27: error: cannot infer type arguments for EnumMap<>
    private static final Map<SomeEnum, String> TEST = new EnumMap<>(Arrays.stream(SomeEnum.values())
                                                      ^

I have found that this can be worked around by replacing t -> t with Function.identity() or (SomeEnum t) -> t, but I'm not understanding why this is the case. What limitation in javac is causing this behavior?

I originally found this issue with java 8, but have verified it still occurs with the java 11 compiler.

Damning answered 14/2, 2019 at 4:15 Comment(14)
Make sure you test with javac, not the eclipse compiler. Eclipse compiler compiles this just fine.Damning
Doesn't compile for me in both Intellij and with javac, both using Java 11.0.2Fadge
Intellij using java 1.8 intially displays the code as fine, but when actually compiling and running with javac this breaks during compile.Faunia
Moving to comments, what also fixes this is specifying the Supplier explicitly new EnumMap<>( Arrays.stream(SomeEnum.values()) .collect(Collectors.toMap(t -> t, a -> "", (x, y) -> x, () -> new EnumMap<>(SomeEnum.class))));. References from Holger's answerChapa
There are many workarounds, I listed a few. Function.identity() is probably the simplest, and even cleaner than the original code. However my question is why this happens. I'm aware it's possible to avoid the issue.Damning
Again from Holger and possibly relevant answer linked with further detailed answers for a great explanation.Chapa
works fine with Java8 in eclipse IDE.Starveling
Have you seen the first comment? Make sure you test with javac, not the eclipse compiler. Eclipse compiler compiles this just fine.Faunia
Such type inference errors are not uncommon when I code. I have given up (for now at least) on understanding precisely how far Java’s type inference goes and when it needs help (read (SomeEnum t) -> t). I give it the help it requires and all is well.Symbolism
Can someone explain why this question is deemed useful (entertaining maybe, but useful)? Even if someone comes along and meticulously writes up the JLS justification for this type inference limitation, what is anyone going to be able to do with this specific occurrence?Ungotten
@Ungotten there are actually a bunch of people that like that, like seriously like. this isn't a place just for "useful"Jacobi
this certainly looks like a bug to me, because if you change to HashMap<>(.... it will workJacobi
@Jacobi EnumMap(Map<K, ? extends V> m) compared to HashMap(Map<? extends K, ? extends V> m).. could the key type be the issue?Chapa
If we explicitly mention type instead of diamond operator then it compiles successfully i.e. private static final Map<SomeEnum, String> TEST = new EnumMap<SomeEnum, String>(...Pep
I
9

We can simplify the example further:

Declaring a method like

static <K,V> Map<K,V> test(Map<K,? extends V> m) {
    return Collections.unmodifiableMap(m);
}

the statement

Map<SomeEnum, String> m = test(Collections.emptyMap());

can be compiled without problems. Now, when we change the method declaration to

static <K extends Enum<K>,V> Map<K,V> test(Map<K,? extends V> m) {
    return Collections.unmodifiableMap(m);
}

we get a compiler error. This indicates that the difference between wrapping your stream expression with new EnumMap<>(…) and new HashMap<>(…) lies in the type parameter declaration of the key type, as EnumMap’s key type parameter has been declared as K extends Enum<K>.

It seems to be connected with the self-referential nature of the declaration, e.g. K extends Serializable does not cause an error while K extends Comparable<K> does.

While this fails in all javac versions from Java 8 to Java 11, the behavior is not as consistent as it seems. When we change the declaration to

static <K extends Enum<K>,V> Map<K,V> test(Map<? extends K,? extends V> m) {
    return Collections.unmodifiableMap(m);
}

the code can be compiled again under Java 8, but still fails with Java 9 to 11.

To me, it’s illogical that the compiler infers SomeEnum for K (which would match the bound Enum<K>) and String for V, but fails to infer these types when a bound has been specified for K. So I consider this a bug. I can’t preclude that there’s a statement somewhere in the depth of the specification which allows to conclude that a compiler should behave that way, but if so, the specification should be fixed as well.

As said by others in the comments section, this code can be compiled with Eclipse without problems.

Intractable answered 14/2, 2019 at 9:47 Comment(0)
P
-2

If we explicitly mention type instead of using diamond operator then it compiles successfully. Following is code for the same:

private static final Map<SomeEnum, String> TEST = new EnumMap<SomeEnum, String>(
            Arrays.stream(SomeEnum.values())
                    .collect(Collectors.toMap(t -> t, a -> "")));

For the reference that as per another link, in some scenarios, diamond operator is not supported. It could be further dig down if code snippet in question here lands in this bucket.

Pep answered 14/2, 2019 at 9:30 Comment(1)
It is pretty obvious that the error cannot infer type arguments will go away the moment that we explicitly provide the type arguments. The question is why the compiler cannot infer the type arguments in this very specific case.Gilbertgilberta

© 2022 - 2024 — McMap. All rights reserved.