当前位置: 首页 > 知识库问答 >
问题:

Java Collectors.GroupingBy可以返回一个流作为其分组项列表吗?

晏正豪
2023-03-14
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 都是iEnumerable 而不是list 。(它还具有.key属性。)

groupby显然必须先完全枚举输入项,然后才能发出单个分组,但是,由于它发出的是IEnumerable 而不是List ,如果不枚举整个分组,就可能会有性能上的好处,比如您只执行了.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流中,要分组,必须收集,而不能仅仅给groupingby收集器第二个lambda来提取值。如果您想要一个不同于整个输入的值,则必须重新映射(但请注意,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. 为什么我们必须收集到完全枚举的类型?有没有办法模拟C#的GroupByIEnumerable值类型,并从Collectors.Mapping()返回Map ?还是因为值项必须完全枚举,所以没有用?或者,我们是否可以编写自己的集合器。groupingby,它在第二个参数中使用lambda,并为我们完成这项工作,使语法更接近LINQ的groupby,并且至少具有更清晰的语法,可能还会略微提高性能
  3. 作为一个理论练习,即使没有实际用处,是否可以编写我们自己的Java流收集器toStream(),该收集器返回,并且在枚举之前不迭代其输入(每次迭代一个元素,延迟)?

共有1个答案

狄望
2023-03-14

虽然这些操作在某些方面看起来很相似,但它们是根本不同的。与LINQ的groupby操作不同,Java的groupingby是一个collector,设计用于与流API的终端操作collector协同工作,该终端操作本身不是中间操作,因此通常不能用于实现惰性流操作。

groupingby收集器将另一个下游收集器用于组,因此,在最好的情况下,您可以指定一个收集器就地执行该操作,而不是通过组的元素进行流式传输来执行另一个操作。虽然这些收集器不支持短路,但它们不需要将组收集到lists中,只需要对其进行流式处理。例如,groupingby(f1,summingInt(f2))。将组收集到列表中的情况已经被认为是很常见的,当您不指定收集器时,可以使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()的收集器。

为了完整起见,下面是一个尝试实现惰性分组的解决方案:

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));
 类似资料:
  • 我有一个方法,它接受一个内部有的对象: MatchData k可以是null,有时k.getWatchListDetail()也可以是null。 我需要检查两种情况。首先,如果它能抛出NPE。 上面的实现可以做到这一点,但我尝试使用或带有链接的流,所以我可以在一行链接中完成。

  • 方法名称: 如果可以将数组拆分为两个,且值的总和相等,则返回true,例如: 不允许更改数组顺序,只允许递归不允许循环,私有方法也可以,只要是递归的。 我写的内容(代码不完整): 我想做的是:我使用一个私有方法来求整个数组的和,然后从总和中减去。主方法中假设逐步求和数组的和。我的问题是这个方法是布尔的,我不知道如何递归地使用布尔方法来完成它。 我的问题:你能告诉我结构是否好吗?我该怎么做?

  • 我正在编写一个code-gen工具,用于使用Spring-Data-Jpa为Spring-boot应用程序生成后端连接代码,CrudRepository中的方法返回Iterable而不是List,这让我有点恼火,因为Iterable没有提供足够的功能,但是List提供了,所以我正在寻找将Iterable转换为List的最佳方法。 我看到了这篇关于将可迭代转换为集合的文章,我想知道,与其使用像Gua

  • 如何使用 Java 8 实现一个函数来获取一定数量的流,并生成一个流,其中每个元素都是由流的笛卡尔乘积的一个成员组成的列表? 我看过这个问题 - 这个问题使用了一个聚合器,它是一个(采用两个类似类型的项目并生成一个相同类型的项目)。我希望最终结果中的项目是,而不是输入流中元素的类型。 具体来说,假设我想要的函数称为,如下所示: 应打印: 理想情况下,我希望此操作尽可能懒惰。例如,如果输入流是由 生

  • 问题内容: 我有一个由Java 8流表示的数据集: 我可以看到如何对其进行过滤以获取随机子集-例如 我还可以看到如何减少该流,例如得到两个表示数据集的两个随机一半的列表,然后将它们转换回流。但是,是否有直接方法可以从最初的一个生成两个流?就像是 感谢您的任何见解。 问题答案: 不完全是。您不可能一分之二。这没有道理-您将如何遍历一个而不需要同时生成另一个?流只能操作一次。 但是,如果要将它们转储到

  • 我无意中发现了一些有趣的代码。链接到代码 在我的理解中,应该是真实的,因为typeof返回一个字符串。比如: 发出“hello world”,因为偶数typeof(未定义)= tldr:typeof是否有任何可能的错误结果?