2.2 max:利用归约求最大
max方法也是一个归约方法,它是直接调用了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是Optional对基础类型long的封装。
2.3 min:利用归约求最小
min还是直接看例子吧:
Optional<Integer> max = List.of(1, 2, 3).stream()
.min(Integer::compare);
复制代码
它和max区别就是底层把 >
换成了 <
,过于简单,不再赘述。
3. 收集器
第三节我们来看看收集器,它的作用是对Stream中的元素进行收集而形成一个新的集合。
虽然我在本篇开头的时候已经给过一张思维导图了,但是由于收集器的API比较多所以我又画了一张,算是对开头那张的补充:
收集器的方法名是collect,它的方法定义如下:
<R, A> R collect(Collector<? super T, A, R> collector);
复制代码
顾名思义,收集器是用来收集Stream的元素的,最后收集成什么我们可以自定义,但是我们一般不需要自己写,因为JDK内置了一个Collector的实现类——Collectors。
3.1 收集方法
通过Collectors我们可以利用它的内置方法很方便的进行数据收集:
比如你想把元素收集成集合,那么你可以使用toCollection或者toList方法,不过我们一般不使用toCollection,因为它需要传参数,没人喜欢传参数。
你也可以使用toUnmodifiableList,它和toList区别就是它返回的集合不可以改变元素,比如删除或者新增。
再比如你要把元素去重之后收集起来,那么你可以使用toSet或者toUnmodifiableSet。
接下来放一个比较简单的例子:
// 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的key,我这里指定的是元素中的orderNo。
-
第二个参数代表value,它表示你要设置一个Map的value,我这里直接把元素本身当作值,所以结果是一个Map<String, Order>。
你也可以将元素的属性当作值:
List<Order> orders = List.of(new Order(), new Order());
Map<String, List<Item>> map = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, Order::getItemList));
复制代码
这样返回的就是一个订单号+商品列表的Map了。
toMap() 还有两个伴生方法:
-
toUnmodifiableMap():返回一个不可修改的Map。
-
toConcurrentMap():返回一个线程安全的Map。
这两个方法和toMap() 的参数一模一样,唯一不同的就是底层生成的Map特性不太一样,我们一般使用简简单单的toMap() 就够了,它的底层是我们最常用的HashMap() 实现。
toMap() 功能虽然强大也很常用,但是它却有一个致命缺点。
我们知道HahsMap遇到相同的key会进行覆盖操作,但是toMap() 方法生成Map时如果你指定的key出现了重复,那么它会直接抛出异常。
比如上面的订单例子中,我们假设两个订单的订单号一样,但是你又将订单号指定了为key,那么该方法会直接抛出一个IllegalStateException,因为它不允许元素中的key是相同的。
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。
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类,这里由于我们前面已经使用了Collectors,所以这里不必声明直接传入一个toSet()方法,代表我们将分组后的元素收集为Set。
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 经典复刻方法
终于来到最后一节了,请原谅我给这部分的方法起了一个这么土的名字,但是这些方法确实如我所说:经典复刻。
换言之,就是Collectors把Stream原先的方法又实现了一遍,包括:
-
map →
mapping
-
filter →
filtering
-
flatMap →
flatMapping
-
count →
counting
-
reduce →
reducing
-
max →
maxBy
-
**min ** →
minBy
这些方法的功能我就不一一列举了,之前的文章已经讲的很详尽了,唯一的不同是某些方法多了一个参数,这个参数就是我们在分组和分区里面讲过的收集参数,你可以指定收集到什么容器内。
我把它们抽出来主要想说的为什么要复刻这么多方法处理,这里我说说个人见解,不代表官方意见。
我觉得主要是为了功能的组合。
什么意思呢?比方说我又有一个需求:使用订单类型对订单进行分组,并找出每组有多少个订单。
订单分组我们已经讲过了,找到其每组有多少订单只要拿到对应list的size就行了,但是我们可以不这么麻烦,而是一步到位,在输出结果的时候键值对就是订单类型和订单数量:
Map<Integer, Long> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType, counting()));
复制代码
就这样,就这么简单,就好了,这里等于说我们对分组后的数据又进行了一次计数操作。
上面的这个例子可能不对明显,当我们需要对最后收集之后的数据在进行操作时,一般我们需要重新将其转换成Stream然后操作,但是使用Collectors的这些方法就可以让你很方便的在Collectors中进行数据的处理。
再举个例子,还是通过订单类型对订单进行分组,但是呢,我们想要拿到每种类型订单金额最大的那个,那么我们就可以这样:
文末java面试题,进阶技术大纲,架构资料分享
我将这三次阿里面试的题目全部分专题整理出来,并附带上详细的答案解析,生成了一份PDF文档,有兴趣的朋友们可以点击这里即可免费领取
- 第一个要分享给大家的就是算法和数据结构
- 第二个就是数据库的高频知识点与性能优化
- 第三个则是并发编程(72个知识点学习)
- 最后一个是各大JAVA架构专题的面试点+解析+我的一些学习的书籍资料
子,还是通过订单类型对订单进行分组,但是呢,我们想要拿到每种类型订单金额最大的那个,那么我们就可以这样:
文末java面试题,进阶技术大纲,架构资料分享
我将这三次阿里面试的题目全部分专题整理出来,并附带上详细的答案解析,生成了一份PDF文档,有兴趣的朋友们可以点击这里即可免费领取
- 第一个要分享给大家的就是算法和数据结构
[外链图片转存中…(img-QTXZCdTW-1628441109908)]
- 第二个就是数据库的高频知识点与性能优化
[外链图片转存中…(img-GzbSgPSR-1628441109910)]
- 第三个则是并发编程(72个知识点学习)
[外链图片转存中…(img-gTYXBwjj-1628441109911)]
- 最后一个是各大JAVA架构专题的面试点+解析+我的一些学习的书籍资料
[外链图片转存中…(img-bpMQkc2x-1628441109912)]
还有更多的Redis、MySQL、JVM、Kafka、微服务、Spring全家桶等学习笔记这里就不一一列举出来