Java Collectors.groupingBy可以将Stream作为其分组项列表返回吗?
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Java Collectors.groupingBy可以将Stream作为其分组项列表返回吗?,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含7852字,纯文字阅读大概需要12分钟。
内容图文
![Java Collectors.groupingBy可以将Stream作为其分组项列表返回吗?](/upload/InfoBanner/zyjiaocheng/789/4aad563015384e21b80d118d27fed5a8.jpg)
在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>而不是列表< 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来获取组项目的价值?
>为什么我们必须收集完全枚举类型?有没有办法模拟C#的GroupBy的IEnumerable值类型并返回Map< Integer,Stream< String>>来自Collectors.mapping(),还是没用,因为值项必须完全枚举,无论如何?或者我们可以编写自己的Collectors.groupingBy为第二个参数获取lambda并为我们完成工作,使语法更接近Linq的GroupBy并且至少具有更清晰的语法并且可能略微提高性能?
>作为理论练习,即使没有实际用处,也可以编写我们自己的Java Stream Collector toStream(),它返回一个Stream并且不迭代它的输入,直到并且除非它被枚举(一次迭代一个元素,延迟)?
解决方法:
虽然这些操作在某些方面看起来相似,但它们根本不同.与Linq的GroupBy操作不同,Java的groupingBy是一个收集器,旨在与Stream API的终端操作收集一起工作,这不是一个中间操作本身,因此,通常不能用于实现延迟流操作.
groupingBy收集器使用另一个下游收集器作为组,因此,不是通过组的元素进行流式传输,而是执行另一个操作,您可以指定一个收集器就地执行该操作,在最好的情况下.虽然这些收集器不支持短路,但它们不需要将组收集到列表中,只是为了流过它们.考虑一下,例如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 with() – >新的TreeMap<>(customComparator)您可以获得与默认HashMap完全不同的组(例如,String.CASE_INSENSITIVE_ORDER).另一方面,当您提供EnumMap时,您可能无法获得不同的语义,但完全不同的性能特征.
相比之下,你所描述的Linq的GroupBy操作看起来像是一个在Stream API中根本没有吊坠的中间操作.正如你自己建议的那样,当第一个元素被轮询时,它仍然可以完全遍历,完全填充幕后的数据结构.即使实现尝试了一些懒惰,结果也是有限的.您可以廉价地获得第一组的第一个元素,但如果您只对该元素感兴趣,则根本不需要分组.第一组的第二个元素可能已经是源流的最后一个元素,需要完整的遍历和存储.
所以提供这样的操作意味着一些复杂性,而不是急切地收集.也很难想象并行能力的实现(提供收集操作的好处).实际上的不便不是源于这个设计决定,而是源于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));
这提供了更好的懒惰(例如,如果比赛不在第一组内).
当然,这个例子是设计的,但是我有强烈的感觉,几乎所有具有延迟处理潜力的操作,即不需要所有组并且不需要至少一个组的所有元素,都可以被重写为操作根本不需要分组.
内容总结
以上是互联网集市为您收集整理的Java Collectors.groupingBy可以将Stream作为其分组项列表返回吗?全部内容,希望文章能够帮你解决Java Collectors.groupingBy可以将Stream作为其分组项列表返回吗?所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。