Java Stream 终止操作:缩减、分组和拆分
思维导图城市建设。
在上一篇文章中,我给大家讲了Stream的前半部分知识——包括Stream的整体概况、Stream的创建以及Stream转换流程的运作,并对Stream内部的一些优化点进行了简要说明。
虽然已经晚了,但我还是会继续向大家通报Stream的第二部分知识——终止行动。由于这部分API丰富且复杂,我会单独写一篇文章来详细讲解。
在正式开始之前,我们先来说一下聚合方法本身的特点(我会以聚合方法作为最终操作方法的参考):
- 聚合方法代表的是最终的结果整个流程计算,所以没有一个返回值是Stream。
- 聚合方法的返回值可以为空,例如过滤器不匹配,JDK8中使用Optional来避免NPE。
- 所有聚合方法都会调用evaluate方法。这是一个内部方法。您可以通过查看源代码来使用它来确定某个方法是否是聚合方法。
ok,既然知道了聚合方法的特点,为了方便理解,我把聚合方法分为几类:
其中,我简单解释一下简单的聚合方法,重点介绍其他的,尤其是收藏家。他能做的事情有很多。 。 。
Stream聚合方法是我们使用Stream时必要的操作。如果你仔细学习这篇文章,你是否能够立即掌握Stream,至少能够流利地掌握Stream?
1.简单的聚合方法
首先,让我们从简单的事情开始。
Stream的聚合方法比上一篇提到的更加无状态和有状态,不过有些喵喵一看就学会了。我们先来说第一部分这些方法:
count()
:返回Stream中元素的大小。forEach()
:通过Stream中所有元素的内循环消耗每个元素。该方法没有返回值。forEachOrder()
:效果和上面方法一样,但是可以保持消费的顺序,即使在多线程环境下也是如此。任何匹配(谓词)
:这是一个快捷操作。通过传递断言参数来确定元素是否可以与断言匹配。allMatch(predicate)
:这是一个快捷操作,通过指定断言参数返回是否所有元素都可以匹配断言。noneMatch(谓词)
:这是一个快捷操作。通过指定断言参数来指定是否所有元素都无法与断言匹配。如果是则返回 true,否则返回 false。findFirst()
:这是一个快捷操作,返回 Stream 中的第一个元素。 Stream 可以为空,因此返回值由可选值处理。findAny()
:这是一个返回字符串中任意元素的快捷操作。它通常是字符串中的第一个元素。字符串可以为空,因此返回值使用可选值进行处理。
虽然上面很简单,但是有五个涉及快捷操作的方法我要提一下:
第一个是
findFirst()
和这两个方法,因为它们只需要终止一个元件,短路效应很容易理解。
然后是方法anyMatch
,只需要匹配一个元素即可完成,所以它的短路效果也很容易理解。
最后,还有方法 allMatch
和 noneMatch
。乍一看,这两个方法必须遍历整个流中的所有元素。事实上,事实并非如此。例如,allMatch只需要一个元素。如果与断言不匹配,它可以返回 false,而如果有一个元素与断言匹配,noneMatch 也可以返回 false,因此这两种方法都具有短路效果。
2。缩减
2.1 缩减:重复评估
第二部分我们会讲缩减。因为这个词太抽象了,所以我得找一个通俗易懂的解释来翻译这句话。换句话说,下面是归约的定义:
反复组合Stream中的所有元素得到一个结果,这样的操作称为归约。
注意:在函数式编程中,这称为折叠。
举一个很简单的例子,我有三个元素1、2、3,我把它们相加,最后得到数字6。这个过程就是归约。
再比如,我有三个元素1、2、3,我比较它们,最后选择最大的数3或者最小的数1。这个过程也是一个归约。
下面我将给出一个总结示例来演示减少。归约使用归约的方法:
Optional<Integer> reduce = List.of(1, 2, 3).stream() .reduce((i1, i2) -> i1 + i2);
首先你可能注意到了,在上面的小例子中我用了“亮亮”这个词,意思就是归约。大约处理两个元素,然后得到最终值,因此reduce方法的参数是一个二进制表达式,对两个参数进行任意处理,最终得到一个结果,其参数和结果必须是同一类型。
例如,在代码中,i1和i2是二进制表达式的两个参数。它们代表一个元素中的第一个元素和第二个元素。第一次加法完成后,结果会赋值给i1,i2继续代表下一个元素,直到元素用完,得到最终结果。
如果觉得这段文字写得不够优雅,也可以使用 Integer 中默认的方法:
Optional<Integer> reduce = List.of(1, 2, 3).stream() .reduce(Integer::sum);
这也是使用 方法引用
来表达 lambda 的例子表达。
您可能还会注意到,它们的返回值是可选的,以避免 Stream 没有元素的情况。
你还可以想办法摆脱这种情况,即元素中必须至少有一个值。这里reducer给了我们一个重载方法:
Integer reduce = List.of(1, 2, 3).stream() .reduce(0, (i1, i2) -> i1 + i2);
如上面的例子,在二元表达式之前添加了一个额外的参数,这个参数称为初始值,所以即使你的Stream没有元素,它最终也会返回0 ,所以不需要可选的。
在该方法的实际执行中,第一次执行时初始值会占据位置 i1,i2 代表 Stream 中的第一个元素,然后所得的和再次占据位置 i1,i2 代表下一个元素。
但是,使用初始值并不是没有成本的。它应该满足原则: accumulator.apply(identity, i1) == i1
,这意味着当它第一次执行时,它的返回结果应该是你的 Stream 的第一个元素。
比如我上面的例子是一个加法运算。第一个加法是0 + 1 = 1
,与上述原理一致。这个原则是为了保证在并行流的情况下得到正确的结果。
如果你的初始值为1,那么当前情况下每个线程都初始化为1,那么你最终的总数会比你预期的要大。
2.2 max:使用归约求最大值。
max方法也是直接调用reduce方法的reduce方法。
先看一个例子:
Optional<Integer> max = List.of(1, 2, 3).stream() .max((a, b) -> { if (a > b) { return 1; } else { return -1; } });
是的,max方法就是这样使用的,这让我感觉我没有使用函数式接口。当然,你也可以用Integer的方法来化简:
Optional<Integer> max = List.of(1, 2, 3).stream() .max(Integer::compare);
即便如此,这个方法在我看来还是很麻烦。虽然我知道在 max 方法中传递参数允许我们自定义排序规则,但我不明白为什么没有默认的自然排序方法。让我输入参数。
后来我才想到基本的Stream型。当然可以,不传参数直接取最大值即可:
OptionalLong max = LongStream.of(1, 2, 3).max();
当然,类库的设计者已经想到了我能想到的一切~
注:OptionalLong 是对基本类型OptionalLong 的封装。 ?很简单,无需赘述。
3。收集器
第三部分,我们来看看收集器。它的功能是收集Stream的元素,形成一个新的整体。
虽然我在本文开头已经给出了思维导图,但由于收集器的 API 比较多,所以我又画了一个与开头补充的:
Collector 方法名称为collect,它的方法定义如下:
<R, A> R collect(Collector<? super T, A, R> collector);
顾名思义,收集器是用来收集Stream的元素的。我们可以自定义最终的集合,但一般情况下我们不需要自己编写,因为JDK有一个内置的实现类Collector——Collectors。 ?使用toCollection是因为它需要传递参数,而没有人喜欢传递参数。
您还可以使用 toUnmodifyingList。它与toList的区别在于,它返回的集合不能更改元素,例如删除或添加新元素。
再举个例子,如果你想在去除重复后收集元素,那么你可以使用toSet或toUnmodifyingSet。
这是一个相当简单的例子:
// toList List.of(1, 2, 3).stream().collect(Collectors.toList()); // toUnmodifiableList List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableList()); // toSet List.of(1, 2, 3).stream().collect(Collectors.toSet()); // toUnmodifiableSet List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableSet());
上述方法没有参数,可以立即使用。底层toList也是经典的ArrayList,底层toSet是经典的HashSet。
有时候你可能希望将其收集到一个Map中,例如将订单数据转换为订单对应的订单号,那么你可以使用toMap():
List<Order> orders = List.of(new Order(), new Order()); Map<String, Order> map = orders.stream() .collect(Collectors.toMap(Order::getOrderNo, order -> order));
toMap() 有两个参数:
- 第一个参数代表 key ,表示要设置地图键。这里我指定了元素中的订单号。
- 第二个参数代表一个值,表示你要设置地图的值。我直接使用元素本身作为值,因此结果是 Map。
也可以使用元素属性作为值:
List<Order> orders = List.of(new Order(), new Order()); Map<String, List<Item>> map = orders.stream() .collect(Collectors.toMap(Order::getOrderNo, Order::getItemList));
返回订单号图+商品列表。
toMap() 有两个配套方法:
- toUnmodifyingMap():返回不可修改的映射。
- toConcurrentMap():返回一个线程安全的映射。
这两个方法的参数与toMap()完全相同。唯一的区别是底层生成的地图的特征不同。我们一般使用一个简单的toMap(),它的底层是我们最常用的层。 HashMap() 的实现。
toMap() 虽然功能强大且常用,但它有一个致命的缺陷。
我们知道HahsMap遇到相同的key时会被覆盖,但是如果toMap()方法生成map时你指定的key重复,则直接抛出异常。
例如上面的订单示例,我们假设两个订单的序号相同,但是如果你指定订单号作为key,那么这个方法会直接抛出IllegalStateException,因为它不允许指定元素相同。
3.2 分组方法
如果你想对数据进行分类,但你指定的key可能会重复,那么你应该使用groupingBy而不是toMap。
举个简单的例子,我想按照订单类型对订单集合进行分组,那么可以这样做:
List<Order> orders = List.of(new Order(), new Order()); Map<Integer, List<Order>> collect = orders.stream() .collect(Collectors.groupingBy(Order::getOrderType));
直接指定用于分组的元素的属性,它会自动按该属性进行分组,并且分组结果将收集为列表。
List<Order> orders = List.of(new Order(), new Order()); Map<Integer, Set<Order>> collect = orders.stream() .collect(Collectors.groupingBy(Order::getOrderType, toSet()));
groupingBy 还提供了一个重载,允许您自定义收集器类型,因此它的第二个参数是 Collector 对象。
对于Collector类型,我们一般使用Collectors类。由于我们之前使用过收集器,因此无需声明直接传递到这里的 toSet() 方法,这意味着我们将把分组的元素收集到一个集合中。
groupingBy 有一个类似的方法,称为 groupingByConcurrent()。这种方法可以提高并行分组的效率,但是不保证顺序,这里不再赘述。
3.3 划分方法
接下来我将介绍另一种分组情况——划分。这个名字有点令人困惑,但含义很简单:
按 TRUE 或 FALSE 对数据进行分组称为分区。
例如,我们根据是否付款对订单列表进行分组。这是拆分:
List<Order> orders = List.of(new Order(), new Order()); Map<Boolean, List<Order>> collect = orders.stream() .collect(Collectors.partitioningBy(Order::getIsPaid));
因为订单是否支付只有两种状态:已支付和未支付,所以这种分组方式称为拆分。
和groupingBy一样,它也有一个重载的收集器类型匹配方法:
List<Order> orders = List.of(new Order(), new Order()); Map<Boolean, Set<Order>> collect = orders.stream() .collect(Collectors.partitioningBy(Order::getIsPaid, toSet()));
3.4经典复制方法
我们终于到了最后一部分。请原谅我没有命名该方法的这一部分。它有一个如此不起眼的名字,但这些方法正是我所说的:经典的复制品。
也就是说,收藏家们又执行了Stream原来的做法,包括:
- map →
mappingfilter›‼filter❙ flatmap →
地图计数 →
计数
- 减少 →
❙❙减少
- maxBy
- min →
minBy
我不会使用这些方法.一一列举。之前的文章已经详细解释过。唯一的区别是某些方法有一个额外的参数。这个参数就是我们所说的分组和拆分时的参数。通过收集参数,您可以指定应该收集哪个容器。
我提它们主要是想讲一下为什么有这么多的饲养方法。我这里说的是我个人的观点,不代表官方观点。
我觉得主要是为了功能的组合。
这是什么意思?假设我有另一个要求:按订单类型对订单进行分组,并查看每组中有多少订单。
我们已经讨论了订单分组。要了解每个组中有多少个订单,我们只需要获取相应列表的大小即可。但我们不能这么烦人,还是一步到位吧。输出结果时,键值对是有序的。订购的类型和数量:
Map<Integer, Long> collect = orders.stream() .collect(Collectors.groupingBy(Order::getOrderType, counting()));
就是这样,就这么简单,就是这样。这相当于说我们对分组后的数据又进行了一次计数操作。
上面的例子可能不太明显。当我们需要在最终收集后处理数据时,我们通常必须将其转换回 Stream,然后再使用它,但是使用这些收集器方法可以使您变得非常容易。数据处理在收集器中完成。
再比如,我们仍然按照订单类型对订单进行分组,但是如果我们想得到每种类型的订单量最大,那么我们可以这样做:
List<Order> orders = List.of(new Order(), new Order()); Map<Integer, Optional<Order>> collect2 = orders.stream() .collect(groupingBy(Order::getOrderType, maxBy(Comparator.comparing(Order::getMoney))));
这样更加简洁方便,我们不需要分组后一一求最大值,我们可以一步完成。
进一步分组后,求出每组的序号:
List<Order> orders = List.of(new Order(), new Order()); Map<Integer, Long> collect = orders.stream() .collect(groupingBy(Order::getOrderType, summingLong(Order::getMoney)));
不过,我们这里并没有讲 summingLong。它是一个内置的加法运算,支持 Integer、Long 和 Double。
有一个类似的方法,称为averagingLong。只要看一下名字,你就会发现找到平均值相当容易。闲着没事的时候,建议扫两遍。
该结束了,最后一个joining()方法对于连接字符串来说非常实用:
List<Order> orders = List.of(new Order(), new Order()); String collect = orders.stream() .map(Order::getOrderNo).collect(Collectors.joining(","));
这个方法的方法名看起来有点眼熟,是的,String类在JDK8之后新添加了一个 join() 方法
也用于连接字符串。连接收集器具有相同的功能,基本实现也相同,使用 StringJoiner 类。
4。总结
终于完成了。
在这篇有关Stream中最终操作的文章中,我提到了Stream中所有聚合方法。可以说,读完这篇文章你就会对Stream中的所有聚合操作有一个很好的了解。如果您不知道如何使用它们也没关系。只要知道这个东西存在就行了,不然在你的知识体系里Stream根本就做不了XX事,那就有点可笑了。
当然,我还是建议大家在项目中使用这些简洁的API,以提高代码的可读性和清晰度。考试后也很容易给别人留下深刻的印象~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。