Почему этот код не компилируется, ссылаясь на вывод типа в качестве причины?

Вот минимальный пример кода, с которым я работаю:

public class Temp {
    enum SomeEnum {}

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

}

Вывод компилятора:

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

Я обнаружил, что это можно обойти, заменив t → t на Function.identity() или (SomeEnum t) → t, но я не понимаю, почему это так. Какое ограничение в javac вызывает такое поведение?

Первоначально я обнаружил эту проблему с Java 8, но подтвердил, что она по-прежнему происходит с компилятором Java 11.

Ответы

Ответ 1

Мы можем упростить пример далее:

Объявление метода как

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

выражение

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

может быть скомпилировано без проблем. Теперь, когда мы изменим объявление метода на

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

мы получаем ошибку компилятора. Это указывает на то, что разница между переносом выражения вашего потока с new EnumMap<>(…) и new HashMap<>(…) заключается в объявлении параметра типа для типа ключа, так как параметр типа ключа EnumMap был объявлен как K extends Enum<K>.

Похоже, что это связано с самореференциальной природой объявления, например, K extends Serializable не вызывает ошибку, в то время как K extends Comparable<K> делает.

В то время как это терпит неудачу во всех версиях javac от Java 8 до Java 11, поведение не столь согласованно, как кажется. Когда мы меняем объявление на

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

код может быть скомпилирован снова под Java 8, но все еще не работает с Java 9 до 11.

Для меня нелогично, что компилятор выводит SomeEnum для K (который будет соответствовать связанному Enum<K>) и String для V, но не выводит эти типы, когда для K указана граница. Поэтому я считаю это ошибкой. Я не могу исключить, что есть где-то в глубине спецификации утверждение, которое позволяет сделать вывод, что компилятор должен вести себя таким образом, но если это так, то спецификация также должна быть исправлена.

Как говорили другие в разделе комментариев, этот код может быть скомпилирован с Eclipse без проблем.

Ответ 2

Если мы явно указываем тип вместо использования оператора diamond, то он успешно компилируется. Ниже приведен код для того же:

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

Для справки, что по другой ссылке, в некоторых сценариях оператор Diamond не поддерживается. Можно было бы еще покопаться, если фрагмент кода, о котором идет речь, попал в этот сегмент.