Может ли Java Collectors.groupingBy вернуть поток в виде списка сгруппированных элементов? - PullRequest
0 голосов
/ 29 мая 2018

В C # Linq GroupBy возвращает IEnumerable из IGrouping элементов, которые, в свою очередь, являются IEnumerable элементов выбранного типа значения.Вот пример:

var namesAndScores = new Dictionary<string, int>> {
    ["David"] = 90,
    ["Jane"] = 91,
    ["Bill"] = 90,
    ["Tina"] = 89)
};
var IEnumerable<IGrouping<int, string>> namesGroupedByScore =
    namesAndScores
        .GroupBy(
            kvp => kvp.Value,
            kvp => kvp.Key
        );

// Result:
// 90 : { David, Bill }
// 91 : { Jane }
// 89 : { Tina }

В частности, обратите внимание, что каждый IGrouping<int, string> равен IEnumerable<string>, а не, скажем, List<string>.(У него также есть свойство .Key.)

Очевидно, что GroupBy должен полностью перечислить входные элементы, прежде чем он сможет выдать одну группу, однако, поскольку он испускает IEnumerable<string> вместоList<string>, может быть выигрыш в производительности, если вы не перечислите всю группировку, например, если вы только что сделали .First().

За исключением: технически, я полагаю, GroupBy может подождатьпока вы не перечислите его для использования одного элемента из входных данных, затем выделите один IGrouping и перечислите только остальную часть входных данных при перечислении IGrouping, собирая другие группы во внутреннюю структуру данных при поискеследующий элемент в текущей группе, но я считаю, что это маловероятная и проблематичная реализация, и ожидаю, что GroupBy будет полностью перечислять во время вызова.

Вот как будет выглядеть код с First():

 var oneStudentForEachNumericScore = namesGroupedByScore
     .ToDictionary(
         grouping => grouping.Key,
         grouping => grouping.First() // does not fully enumerate the values
     );
 // Result:
 // 90 : David -- Bill is missing and we don't care
 // 91 : Jane
 // 89 : Tina

Теперь в Java Streams для группировки нужно собирать, и вы не можете просто дать сборщику groupingBy вторую лямбду для извлечения значения.Если вам нужно значение, отличное от всего ввода, вам необходимо снова отобразить (хотя обратите внимание, что коллектор groupingBy позволяет создавать многоуровневые группы групп из ... групп за один шаг).Вот код, эквивалентный приведенному выше коду C #:

Map<Integer, List<String>> namesGroupedByScore = namesAndScores
      .entrySet().stream()
      .collect(Collectors.groupingBy(
          Map.Entry::getValue,
          Collectors.mapping(
              Map.Entry::getKey,
              Collectors.toList(),
          )
      ));

Это кажется неоптимальным.Итак, мои вопросы:

  1. Есть ли способ выразить это более просто, без необходимости использовать Collectors.mapping, чтобы получить значение групповых элементов в качестве значения?
  2. Почему мынадо собирать до полностью перечислимого типа?Есть ли способ имитировать IEnumerable тип значения в C # GroupBy и возвращать Map<Integer, Stream<String>> из Collectors.mapping(), или это будет бесполезно, потому что элементы значения должны быть перечислены полностью, в любом случае?Или мы могли бы написать свой собственный Collectors.groupingBy, который принимает лямбду для второго аргумента и выполняет эту работу за нас, делая синтаксис ближе к GroupBy у Linq и имея хотя бы более чистый синтаксис и, возможно, немного улучшенную производительность?
  3. В качестве теоретического упражнения, даже если оно практически бесполезно, возможно ли написать собственный Java Stream Collector toStream(), который возвращает Stream и НЕ выполняет итерацию своего ввода до тех пор, пока он не будет перечислен (итерация по одному элементу за разОтложено)?

Ответы [ 2 ]

0 голосов
/ 31 мая 2018

Вот решения для части ваших вопросов от StreamEx и моей библиотеки AbacusUtil

Map<String, Integer> namesAndScores 
             = N.asMap("David", 90, "Jane", 91, "Bill", 90, "Tina", 89);

// By StreamEx
Map<Integer, List<String>> namesGroupedByScore = EntryStream.of(namesAndScores)
                                .invert().grouping();

// By AbacusUtil
Map<Integer, List<String>> namesGroupedByScore = EntryStream.of(namesAndScores)
                                   .groupTo(Fn.value(), Fn.key());
// Or
Map<Integer, Stream<String>> namesGroupedByScore2 = 
        EntryStream.of(namesAndScores).toMap(Fn.value(), collectingAndThen(mapping(Fn.key()), Stream::of));

Если вы хотите сохранить только имя после группыby:

Map<Integer, List<String>> namesAndScores3 = 
      EntryStream.of(namesAndScores).distinctByValue().groupTo(Fn.value(), Fn.key());
// Or
Map<Integer, String> namesAndScores4 = 
          EntryStream.of(namesAndScores).distinctByValue().toMap(Fn.value(), Fn.key());

Если вы хотите сохранить максимум 2 значения.

Map<Integer, List<String>> namesAndScores5 = EntryStream.of(namesAndScores).toMap(Fn.value(),
        MoreCollectors.mapping(Fn.key(), MoreCollectors.toList(2)));

В остальном я верю тому, что сказал Хольгер: «... но у меня естьсильное ощущение, что почти любая операция, которая несет в себе потенциал отложенной обработки, то есть не нуждается во всех группах и не нуждается во всех элементах хотя бы одной группы, может быть переписана в операцию, которая вообще не нуждается в группировке. "

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

0 голосов
/ 29 мая 2018

Хотя эти операции выглядят похожими в некоторых аспектах, они принципиально отличаются.В отличие от операции Linq GroupBy, Java groupingBy представляет собой Collector, предназначенную для работы с терминальной операцией collect Stream Stream, которая сама по себе не является промежуточной операцией и, следовательно,не может использоваться для реализации операции отложенного потока вообще.

Сборщик groupingBy использует другой нисходящий поток Collector для групп, поэтому вместо потоковой передачи по элементам группы для выполнения другой операции выбудет указывать коллектор, выполняющий эту операцию на месте, в лучшем случае.Хотя эти коллекторы не поддерживают короткое замыкание, они устраняют необходимость собирать группы в List s, просто для их потоковой передачи.Просто подумайте, например, groupingBy(f1, summingInt(f2)).Случай сбора групп в List считается достаточно распространенным, чтобы подразумевать, что toList() подразумевается, когда вы не указываете сборщик, но это не учитывалось в случае отображения элементов перед сбором в список..

Если вы столкнетесь с этим случаем достаточно часто, было бы легко определить свой собственный сборщик

public static <T,K,V> Collector<T,?,Map<K,List<V>>> groupingBy(
    Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {
    return Collectors.groupingBy(key, Collectors.mapping(value, Collectors.toList()));
}

и использовать его как

Map<Integer,List<String>> result = map.entrySet().stream()
    .collect(groupingBy(Map.Entry::getValue, Map.Entry::getKey));

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

Map<Integer,List<String>> result = map.entrySet().stream()
        .collect(groupingBy(kvp -> kvp.getValue(), kvp -> kvp.getKey()));

, но, как уже отмечалось, если вы собираетесь потом пролистывать эту карту и беспокоиться о нелицеприятности этогоВ любом случае вы, возможно, захотите использовать коллектор, отличный от toList().

Хотя этот подход обеспечивает некоторую гибкость в отношении результирующих значений, Map и его ключи являются неизбежной частью этой операции, так кактолько Map обеспечивает логику хранения, его операция поиска также отвечает за формирование групп, что также определяетсемантическая.Например, когда вы используете вариант с поставщиком карт с () -> new TreeMap<>(customComparator), вы можете получить совершенно другие группы, как и по умолчанию HashMap (например, String.CASE_INSENSITIVE_ORDER).С другой стороны, когда вы предоставляете EnumMap, вы можете не получить другую семантику, но совершенно другие характеристики производительности.

В отличие от этого, операция GroupBy из описанного вами Linq выглядит как промежуточная операция, котораяне имеет кулона в Stream API вообще.Как вы сами себе предположили, высоки шансы, что он все же совершит полный обход после опроса первого элемента, полностью заполняя структуру данных за кулисами.Даже если реализация пробует некоторую лень, результаты ограничены.Вы можете дешево получить первый элемент первой группы, но если вас интересует только этот элемент, вам вообще не понадобится группировка.Второй элемент первой группы может уже быть последним из исходного потока, требующего полного обхода и хранения.

Таким образом, предложение такой операции подразумевало бы некоторую сложность с небольшим преимуществом по сравнению со сбором с нетерпением.Также трудно представить реализацию с параллельной поддержкой (предлагая преимущества по сравнению с операцией collect).Фактическое неудобство связано не с этим дизайнерским решением, а с тем фактом, что полученный Map не является Collection (учтите, что реализация Iterable в одиночку не подразумевает наличие stream() метод ) и решение разделить операции сбора и потоковые операции .Эти два аспекта приводят к требованию использовать entrySet().stream() для потоковой передачи по карте, но это выходит за рамки этого вопроса.И, как сказано выше, если вам это нужно, сначала проверьте, не может ли другой нижестоящий коллектор для коллектора groupingBy обеспечить желаемый результат в первую очередь.

Для полноты, вот решение, котороепытается реализовать отложенную группировку:

public interface Group<K,V> {
    K key();
    Stream<V> values();
}
public static <T,K,V> Stream<Group<K,V>> group(Stream<T> s,
    Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {

    return StreamSupport.stream(new Spliterator<Group<K,V>>() {
        final Spliterator<T> sp = s.spliterator();
        final Map<K,GroupImpl<T,K,V>> map = new HashMap<>();
        ArrayDeque<Group<K,V>> pendingGroup = new ArrayDeque<>();
        Consumer<T> c;
        {
        c = t -> map.compute(key.apply(t), (k,g) -> {
            V v = value.apply(t);
            if(g == null) pendingGroup.addLast(g = new GroupImpl<>(k, v, sp, c));
            else g.add(v);
            return g;
        });
        }
        public boolean tryAdvance(Consumer<? super Group<K,V>> action) {
            do {} while(sp.tryAdvance(c) && pendingGroup.isEmpty());
            Group<K,V> g = pendingGroup.pollFirst();
            if(g == null) return false;
            action.accept(g);
            return true;
        }
        public Spliterator<Group<K,V>> trySplit() {
            return null; // that surely doesn't work in parallel
        }
        public long estimateSize() {
            return sp.estimateSize();
        }
        public int characteristics() {
            return ORDERED|NONNULL;
        }
    }, false);
}
static class GroupImpl<T,K,V> implements Group<K,V> {
    private final K key;
    private final V first;
    private final Spliterator<T> source;
    private final Consumer<T> sourceConsumer;
    private List<V> values;

    GroupImpl(K k, V firstValue, Spliterator<T> s, Consumer<T> c) {
        key = k;
        first = firstValue;
        source = s;
        sourceConsumer = c;
    }
    public K key() {
        return key;
    }
    public Stream<V> values() {
        return StreamSupport.stream(
            new Spliterators.AbstractSpliterator<V>(1, Spliterator.ORDERED) {
            int pos;
            public boolean tryAdvance(Consumer<? super V> action) {
                if(pos == 0) {
                    pos++;
                    action.accept(first);
                    return true;
                }
                do {} while((values==null || values.size()<pos)
                           &&source.tryAdvance(sourceConsumer));
                if(values==null || values.size()<pos) return false;
                action.accept(values.get(pos++ -1));
                return true;
            }
        }, false);
    }
    void add(V value) {
        if(values == null) values = new ArrayList<>();
        values.add(value);
    }
}

Вы можете проверить это на следующем примере:

group(
    Stream.of("foo", "bar", "baz", "hello", "world", "a", "b", "c")
          .peek(s -> System.out.println("source traversal: "+s)),
        String::length,
        String::toUpperCase)
    .filter(h -> h.values().anyMatch(s -> s.startsWith("B")))
    .findFirst()
    .ifPresent(g -> System.out.println("group with key "+g.key()));

, который напечатает:

source traversal: foo
source traversal: bar
group with key 3

, показывая, чтолень работает как можно дальше.Но

  • Каждая операция,требуется знать, что все группы / ключи требуют полного обхода источника, поскольку самый последний элемент может вводить новую группу
  • Каждая операция, которая требует обработки всех элементов хотя бы одной группы, требует полного обхода,поскольку самый последний элемент источника может принадлежать к этой группе
  • Предыдущий пункт относится даже к операциям с коротким замыканием, если они не могут остановиться рано.Например, в приведенном выше примере поиск соответствия во второй группе подразумевает неудачный полный обход первой группы, следовательно, полный обход источника
  • Приведенный выше пример можно переписать в

    Stream.of("foo", "bar", "baz", "hello", "world", "a", "b", "c")
          .peek(s -> System.out.println("source traversal: "+s))
          .filter(s -> s.toUpperCase().startsWith("H"))
          .map(String::length)
          .findFirst()
          .ifPresent(key -> System.out.println("group with key "+key));
    

    , который предлагает еще большую лень (например, если совпадение не в первой группе).

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

...