在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
收集器提供第二个lambda来提取值。如果您想要一个不同于整个输入的值,则必须再次映射(但请注意,groupingBy
收集器允许您在一个步骤中创建多组...组的多组)。这是上述C#代码的等效代码:
Map<Integer, List<String>> namesGroupedByScore = namesAndScores
.entrySet().stream()
.collect(Collectors.groupingBy(
Map.Entry::getValue,
Collectors.mapping(
Map.Entry::getKey,
Collectors.toList(),
)
));
这似乎不太理想。所以我的问题是:
Collectors.mapping
来获取组项目的价值?IEnumerable
的{{1}}值类型并从GroupBy
返回Map<Integer, Stream<String>>
,或者这样做没有用,因为值无论如何,必须完全枚举项目?或者我们可以编写自己的Collectors.mapping()
,它为第二个参数获取lambda并为我们完成工作,使语法更接近Linq&#39; Collectors.groupingBy
并且至少具有更清晰的语法并且可能略微性能提升?GroupBy
,它返回toStream()
并且不会迭代其输入,除非它被枚举(一次迭代一个元素,延期)?答案 0 :(得分:3)
虽然这些操作在某些方面看起来相似,但它们根本不同。与Linq的GroupBy
操作不同,Java的groupingBy
是Collector
,旨在与Stream API的终端操作 collect
一起使用,这是一个不是中间操作本身,因此,通常不能用于实现惰性流操作。
groupingBy
收集器使用另一个下游Collector
作为组,因此,不是通过组的元素进行流式传输,而是执行另一个操作,您可以指定一个收集器就地执行该操作,最好的情况。虽然这些收集器不支持短路,但它们不需要将组收集到List
中,只是为了流过它们。考虑一下,例如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
不仅提供存储逻辑,其查找操作也是还负责形成群体,这也决定了语义。例如。当您将the variant with a map supplier与() -> new TreeMap<>(customComparator)
一起使用时,您可能会获得与默认HashMap
完全不同的群组(例如String.CASE_INSENSITIVE_ORDER
)。另一方面,当您提供EnumMap
时,您可能无法获得不同的语义,但完全不同的性能特征。
相比之下,你所描述的Linq的GroupBy
操作看起来像是一个在Stream API中根本没有吊坠的中间操作。正如你自己建议的那样,当第一个元素被轮询时,它仍然可以完全遍历,完全填充幕后的数据结构。即使实现尝试了一些懒惰,结果也是有限的。您可以廉价地获得第一组的第一个元素,但如果您只对该元素感兴趣,则根本不需要分组。第一组的第二个元素可能已经是源流的最后一个元素,需要完整的遍历和存储。
因此提供这样的操作意味着一些复杂性,而不是急于收集的好处。也很难想象并行能力的实现(提供优于collect
操作的优势)。实际的不便不是来自这个设计决定,而是源于结果Map
is not a Collection
(注意单独实施Iterable
wouldn’t imply having a stream()
method)和决定to separate collection operations and stream operations。这两个方面导致需要使用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));
提供更好的懒惰(例如,如果匹配不在第一组内)。
当然,这个例子是设计的,但我有强烈的感觉,几乎所有具有延迟处理潜力的操作,即不需要所有组,并且不需要至少一个组的所有元素,都可以被重写进入一个根本不需要分组的操作。
答案 1 :(得分:0)
以下是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));
如果您只想在分组后保存名字:
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)));
对于其他问题,我相信Holger所说的话:“......但我有强烈的感觉,几乎任何具有懒惰处理潜力的操作,即不需要所有组,也不需要所有元素至少有一个组,可以重写为一个根本不需要分组的操作。“
在任何情况下,如果需要groupBy
,我认为在没有迭代所有元素的情况下存在这样的实现,无论你使用哪种语言。如果不需要迭代所有元素,则很可能groupBy
不必要或被滥用。