Java Streams GroupingBy и фильтрация по количеству (аналогично SQL HAVING) - PullRequest
7 голосов
/ 23 апреля 2020

Поддерживают ли потоки Java (9+) условие HAVING, подобное SQL? Вариант использования: группировка, а затем отбрасывание всех групп с определенным количеством. Можно ли написать следующее SQL предложение в виде Java stream?

GROUP BY id
HAVING COUNT(*) > 5

Наиболее близким, что я мог придумать, было:

input.stream()
        .collect(groupingBy(x -> x.id()))
        .entrySet()
        .stream()
        .filter(entry -> entry.getValue().size() > 5)
        .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));

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

Я вижу, что есть collectingAndThen и filtering коллекторы, но я не знаю, они решат мою проблему (точнее, как правильно их применить).

Есть ли лучшая (более идиоматическая c) версия вышеупомянутого, или я застрял со сбором на промежуточную карту, отфильтровав это а потом собирать на финальную карту?

Ответы [ 3 ]

7 голосов
/ 24 апреля 2020

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

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

Map<KeyType, List<ElementType>> result =
    input.stream()
        .collect(collectingAndThen(groupingBy(x -> x.id(), HashMap::new, toList()),
            m -> {
                m.values().removeIf(l -> l.size() <= 5);
                return m;
            }));

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

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

public static <T,K,V> Collector<T,?,Map<K,V>> having(
                      Collector<T,?,? extends Map<K,V>> c, BiPredicate<K,V> p) {
    return collectingAndThen(c, in -> {
        Map<K,V> m = in;
        if(!(m instanceof HashMap)) m = new HashMap<>(m);
        m.entrySet().removeIf(e -> !p.test(e.getKey(), e.getValue()));
        return m;
    });
}

Для большей гибкости этот коллектор позволяет использовать произвольный коллектор, создающий карту, но, поскольку это не обеспечивает тип карты, он будет применять изменяемый карта впоследствии, просто используя конструктор копирования. На практике этого не произойдет, поскольку по умолчанию используется HashMap. Это также работает, когда вызывающая сторона явно запрашивает LinkedHashMap для поддержания порядка. Мы могли бы даже поддержать больше случаев, изменив строку на

if(!(m instanceof HashMap || m instanceof TreeMap
  || m instanceof EnumMap || m instanceof ConcurrentMap)) {
    m = new HashMap<>(m);
}

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

Теперь пользовательский коллектор можно использовать как

Map<KeyType, List<ElementType>> result =
    input.stream()
        .collect(having(groupingBy(x -> x.id()), (key,list) -> list.size() > 5));
2 голосов
/ 24 апреля 2020

Единственный способ, которым я знаю, - это использовать Collectors.collectingAndThen с той же реализацией внутри функции finisher:

Map<Integer, List<Item>> a = input.stream().collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Item::id),
        map -> map.entrySet().stream()
                             .filter(e -> e.getValue().size() > 5)
                             .collect(Collectors.toMap(Entry::getKey, Entry::getValue))));
0 голосов
/ 23 апреля 2020

Если вы хотите более читаемый код, вы также можете (в качестве альтернативы повторного потока) использовать функцию Guava filterValues.

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

Map<A,B> unfiltered = java stream groupingby
return Maps.filterValues(unfiltered, value -> value.size() > 5);
...