Компаратор в коллекторе в потоке вызывает проблемы с выводом типа? - PullRequest
0 голосов
/ 27 ноября 2018

У меня есть следующий упрощенный пример, который группирует Список строк по категориям в форме TreeMap из Integer в List

public static void main(String[] args)
{
    List<String> list = Arrays.asList("A", "B", "C", "D", "E");

    TreeMap<Integer, List<String>> res = list.stream()
        .collect(Collectors.groupingBy(
            s -> s.charAt(0) % 3,
            () -> new TreeMap<>(Comparator.<Integer>reverseOrder()), // Type required
            Collectors.toList()
        ));

    System.out.println(res);
}

Если я не указываю тип Comparator.reverseOrder() код не будет скомпилирован (см. нижнюю часть сообщения об ошибке).

Если я явно указываю тип TreeMap вместо типа Comparator.reverseOrder (), код работает нормально.

() -> new TreeMap<Integer, List<String>>(Comparator.reverseOrder()), // Type required

Итак:

  • Компилятор может вывести тип TreeMap
  • Компилятор может вывести тип Компаратора, если он знаеттип TreeMap
  • Но компилятор не может определить тип Comparator, если он должен определить тип TreeMap.

Я не понимаю, почемукомпилятор не может вывести оба типа.Я проверил это как с JDK 1.8.0_191 из Oracle, так и с JDK 11.0.1_13 из AdoptOpenJDK, с одинаковыми результатами.

Это какое-то ограничение, о котором я не знаю?

Error:(22, 32) java: no suitable method found for groupingBy((s)->s.cha[...]) % 3,()->new Tr[...]er()),java.util.stream.Collector<java.lang.Object,capture#1 of ?,java.util.List<java.lang.Object>>)
    method java.util.stream.Collectors.<T,K>groupingBy(java.util.function.Function<? super T,? extends K>) is not applicable
      (cannot infer type-variable(s) T,K
        (actual and formal argument lists differ in length))
    method java.util.stream.Collectors.<T,K,A,D>groupingBy(java.util.function.Function<? super T,? extends K>,java.util.stream.Collector<? super T,A,D>) is not applicable
      (cannot infer type-variable(s) T,K,A,D
        (actual and formal argument lists differ in length))
    method java.util.stream.Collectors.<T,K,D,A,M>groupingBy(java.util.function.Function<? super T,? extends K>,java.util.function.Supplier<M>,java.util.stream.Collector<? super T,A,D>) is not applicable
      (inferred type does not conform to upper bound(s)
        inferred: java.lang.Object
        upper bound(s): java.lang.Comparable<? super T>,T,java.lang.Object)

Ответы [ 2 ]

0 голосов
/ 28 ноября 2018

К сожалению, вывод типа имеет действительно сложную спецификацию, поэтому очень трудно решить, соответствует ли конкретное нечетное поведение спецификации или это просто ошибка компилятора.

Существует два хорошо известных намеренныхограничения на вывод типа.

Во-первых, целевой тип выражения не используется для выражений получателя, т. е. в цепочке вызовов методов.Поэтому, когда у вас есть оператор вида

TargetType x = first.second(…).third(…);

, TargetType будет использоваться для вывода универсального типа вызова third() и его выражений аргументов, но не для вызова second(…).Таким образом, вывод типа для second(…) может использовать только автономный тип first и выражения аргумента.

Это не проблема здесь.Поскольку автономный тип list четко определен как List<String>, нет проблем с выводом типа результата Stream<String> для вызова stream(), а проблемный вызов collect является последним вызовом методацепочка, которая может использовать целевой тип TreeMap<Integer, List<String>> для вывода аргументов типа.

Второе ограничение касается разрешения перегрузки.Разработчики языка сделали преднамеренное сокращение, когда дело доходит до циклической зависимости между неполными типами выражений аргументов, которые должны знать фактический целевой метод и его тип, прежде чем они смогут помочь определить правильный метод для вызова.

Этотакже не применяется здесь.В то время как groupingBy перегружен, эти методы отличаются количеством параметров, что позволяет выбрать единственный подходящий метод, не зная типов аргументов.Также можно показать, что поведение компилятора не меняется, когда мы заменим groupingBy другим методом, который имеет предполагаемую подпись, но не перегружен.


Ваша проблема может быть решена с помощью, например,

TreeMap<Integer, List<String>> res = list.stream()
    .collect(Collectors.groupingBy(
        (String s) -> s.charAt(0) % 3,
        () -> new TreeMap<>(Comparator.reverseOrder()),
        Collectors.toList()
    ));

При этом используется лямбда-выражение с явной типизацией для функции группировки, которое, хотя на самом деле не влияет на типы ключа карты, заставляет компилятор находить фактические типы.

Хотя использование лямбда-выражений с явным типом вместо неявно типизированных может иметь значение для разрешения перегрузки метода, как сказано выше, это не должно применяться здесь, поскольку этот конкретный сценарий не является проблемой перегруженных методов.

Как ни странно, даже следующее изменение устраняет ошибку компилятора:

static <X> X dummy(X x) { return x; }
…

TreeMap<Integer, List<String>> res = list.stream()
    .collect(Collectors.groupingBy(
        s -> s.charAt(0) % 3,
        dummy(() -> new TreeMap<>(Comparator.reverseOrder())),
        Collectors.toList()
    ));

Здесь мы не помогаем ни с каким дополнительным явным типом, а также не меняем формальную природу лямбда-выражений, но все же, компилятор внезапно выводит все типы правильно.

Поведениеили, кажется, связано с тем, что лямбда-выражения с нулевым параметром всегда явно типизированы.Поскольку мы не можем изменить природу лямбда-выражения с нулевым параметром, я создал следующий альтернативный метод сборщика для проверки:

public static <T, K, D, A, M extends Map<K, D>>
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                              Function<Void,M> mapFactory,                                 
                              Collector<? super T, A, D> downstream) {
    return Collectors.groupingBy(classifier, () -> mapFactory.apply(null), downstream);
}

Затем, используя неявно типизированное лямбда-выражение, поскольку фабрика карт компилируется без проблем:

TreeMap<Integer, List<String>> res = list.stream()
    .collect(groupingBy(
        s -> s.charAt(0) % 3,
        x -> new TreeMap<>(Comparator.reverseOrder()),
        Collectors.toList()
    ));

, тогда как использование лямбда-выражения с явной типизацией вызывает ошибку компилятора:

TreeMap<Integer, List<String>> res = list.stream()
    .collect(groupingBy(                           // compiler error
        s -> s.charAt(0) % 3,
        (Void x) -> new TreeMap<>(Comparator.reverseOrder()),
        Collectors.toList()
    ));

По моему мнению, даже если спецификация поддерживает это поведение, его следует исправить, как следствиепредоставления явных типов никогда не должно заключаться в том, что вывод типов становится хуже, чем без него.Это особенно верно для лямбда-выражений с нулевым аргументом, которые мы не можем превратить в неявно типизированные.

Это также не объясняет, почему превращение всех аргументов в лямбда-выражения с явным типом также устранит ошибку компилятора.

0 голосов
/ 27 ноября 2018

Существуют значительные ограничения на то, что может делать вывод типа Java.Существует множество ситуаций, в которых читатель может сказать, каким должен быть тип, но компилятор не может определить его.Простой случай (который демонстрирует ограничение, аналогичное ограничению в вашем примере):

private static <K,T> T test(K key, Supplier<Map<K,T>> arg) {
    return arg.get().get(key);
}

test("Foo", () -> new TreeMap<>(Comparator.reverseOrder()));

Человек-читатель может увидеть, что компаратор сравнивает строки, потому что тип первого аргумента также является ключом карты, но Java не может сделать выводЭто.Как и в вашем случае, добавление свидетеля типа Comparator.<String> устраняет неоднозначность.

Это естественное следствие системы типизации Java и способа добавления обобщений в язык,Если вам интересно прочитать подробности, см. Спецификация языка Java

...