Stream Two

上一章我们介绍了通过流来处理集合,而它在处理时又主要分为中间操作和终端操作,其中我们使用到collect这个终端操作来将处理过后的数据收集转换成一个List,而这一章我们就将来继续介绍collect这个收集器其他更丰富高效的功能。

我们先来看一个很简单的分组的例子,有一个球队的List集合,其中包含了俱乐部和国家队,现在我们想要将俱乐部和国家队区分到不同的集合中。我们来对比一下,在Java 7和Java 8我们会这么处理这个问题。

List<SoccerTeam> teams = Lists.newArrayList(new SoccerTeam("国际米兰",
                TeamType.CLUB), new SoccerTeam("曼城", TeamType.CLUB),
                new SoccerTeam("中国队", TeamType.COUNTRY), new SoccerTeam("巴塞罗那",
                        TeamType.CLUB));

// java7的形式分组
Map<TeamType, List<SoccerTeam>> result = Maps.newHashMap();
for (SoccerTeam team : teams) {
    if (result.containsKey(team.getTeamType())) {
        result.get(team.getTeamType()).add(team);
    } else {
        result.put(team.getTeamType(), Lists.newArrayList(team));
    }
}

// java8的形式分组
Map<TeamType, List<SoccerTeam>> result = teams.stream().collect(
                Collectors.groupingBy(SoccerTeam::getTeamType));

好吧,我想说,用了Java 8之后,我已经回不去以前的日子了。接下来,我就来一一讲解,收集器的正确使用姿势。

收集
最简单也是最常用的就是我们上一章介绍的对处理好的元素进行收集,转换为我们想要的集合。

// 收集
List<Integer> nums = Lists
                .newArrayList(12343456591);

// 收集奇数并转换为List
System.out.println(nums.stream().filter(n -> n % 2 == 1)
                .collect(Collectors.toList()));// [1, 3, 3, 5, 5, 9, 1]
// 收集奇数并转换为Set
System.out.println(nums.stream().filter(n -> n % 2 == 1)
                .collect(Collectors.toSet()));// [1, 3, 5, 9]

// 转换为map
List<SoccerTeam> teams = Lists.newArrayList(new SoccerTeam("国际米兰",
                TeamType.CLUB), new SoccerTeam("曼城", TeamType.CLUB),
                new SoccerTeam("中国队", TeamType.COUNTRY), new SoccerTeam("巴塞罗那",
                        TeamType.CLUB));
// {巴塞罗那=SoccerTeam(name=巴塞罗那, teamType=CLUB, score=null), 国际米兰=SoccerTeam(name=国际米兰, teamType=CLUB, score=null), 曼城=SoccerTeam(name=曼城, teamType=CLUB, score=null), 中国队=SoccerTeam(name=中国队, teamType=COUNTRY, score=null)}
Map<String, SoccerTeam> teamMap = teams.stream().collect(
                Collectors.toMap(SoccerTeam::getName, team -> team));

// 统计个数
System.out.println(nums.stream().filter(n -> n % 2 == 1)
                .collect(Collectors.counting()));// 7
// 还可以简写为这样
System.out.println(nums.stream().filter(n -> n % 2 == 1).count());// 7

查找最大值和最小值
通过maxByminBy方法可以查找流中的最大值和最小值,需要注意的是,这两个方法返回的是Optional对象,因为流有可能是空的,那就没有任何值了。

// 查找最大值
nums.stream().collect(Collectors.maxBy(Comparator.comparing(n -> n)))
                .ifPresent(System.out::print);// 9
// 查找最小值
nums.stream().collect(Collectors.minBy(Comparator.comparing(n -> n)))
                .ifPresent(System.out::print);// 1

汇总
接下来,你还可以通过summingXXX方法对流中的值求和,通过averagingXXX方法对流中的值求平均数,最后还能通过summarizingXXX方法对流中的值一次性求总和、平均值、最大值和最小值。

// 求和
int sum = nums.stream().collect(Collectors.summingInt(n -> n));// 43
double sum2 = nums.stream().collect(Collectors.summingDouble(n -> n));// 43.0
long sum3 = nums.stream().collect(Collectors.summingLong(n -> n));// 43

// 求平均值
double avg = nums.stream().collect(Collectors.averagingInt(n -> n));// 3.909090909090909
double avg2 = nums.stream().collect(Collectors.averagingDouble(n -> n));// 3.909090909090909
double avg3 = nums.stream().collect(Collectors.averagingLong(n -> n));// 3.909090909090909

// 统计
IntSummaryStatistics stat = nums.stream().collect(Collectors.summarizingInt(n -> n));
//IntSummaryStatistics{count=11, sum=43, min=1, average=3.909091, max=9}
DoubleSummaryStatistics stat2 = nums.stream().collect(Collectors.summarizingDouble(n -> n));
//DoubleSummaryStatistics{count=11, sum=43.000000, min=1.000000, average=3.909091, max=9.000000}
LongSummaryStatistics stat3 = nums.stream().collect(Collectors.summarizingLong(n -> n));
//LongSummaryStatistics{count=11, sum=43, min=1, average=3.909091, max=9}

连接字符串
还可以通过join方法,连接字符串,并且join方法中可以设置连接分隔符

// 连接字符串
System.out.println(Stream.of("hello""java""8").collect(
                Collectors.joining()));// hellojava8
// 使用空格进行分隔
System.out.println(Stream.of("hello""java""8").collect(
                Collectors.joining(" ")));// hello java 8

广义的归约汇总
事实上,上面的所有这些方法都可以通过reducing方法来实现,Collectors.reducing工厂方法是所有这些特殊情况的一般化。例如,可以通过reducing方法来求和。

int reduceSum = nums.stream().collect(
                Collectors.reducing(0, n -> n, (a, b) -> a + b));// 43

它需要三个参数:

  • 第一个参数是归约操作的起始值,也是流中没有元素时的返回值,所以很显然对于数值
    和而言0是一个合适的值。
  • 第二个参数就是将流中的对象转换成int的函数。
  • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。

除此之外,它还有一个简化的单参数版本:

Optional<Integer> opSum = nums.stream().collect(
                Collectors.reducing((a, b) -> a + b));

单参数reducing工厂方法创建的收集器看作三参数方法的特殊情况,它把流中的第一个项目作为起点,把恒等函数(即一个函数仅仅是返回其输入参数)作为一个转换函数。这也意味着,要是把单参数reducing收集器传递给空流的collect方法,收集器就没有起点,所以最后它的返回值是一个Optional对象。

分组
收集器是通过groupingBy函数进行分组,就像是文章最开头所举的例子一样,根据球队的类型,将球队分成不同的组。因为TeamType有现成的方法,直接使用方法引用就可以进行分组,但是如果我们想操作的更复杂一点,例如根本球队不同的积分,将球队分为高中低三组,那就需要通过Lambda表达式来进行分组了。

public enum TeamLevel { High, Middle, Low }
// 根据球队的积分分组
Map<TeamLevel, List<SoccerTeam>> scoreGroup = teams.stream().collect(
                Collectors.groupingBy(team -> {
                    if (team.getScore() > 70) {
                        return TeamLevel.High;
                    } else if (team.getScore() > 30) {
                        return TeamLevel.Middle;
                    }
                    return TeamLevel.Low;
                }));

多级分组
除了上面介绍的通过单一的类别来对流中的数据进行分组,我们还可以根据多个类别来进行多级分组。例如,我们可以先按球队类型进行分组以后,再将不同类型的球队根据积分来进行二次分组。

// 先按球队类型再按照积分进行分组
Map<TeamType, Map<TeamLevel, List<SoccerTeam>>> mulGroup = teams
                .stream().collect(
                        Collectors.groupingBy(SoccerTeam::getTeamType,
                                Collectors.groupingBy((SoccerTeam team) -> {
                                    if (team.getScore() > 70) {
                                        return TeamLevel.High;
                                    } else if (team.getScore() > 30) {
                                        return TeamLevel.Middle;
                                    }
                                    return TeamLevel.Low;
                                })));
{COUNTRY={Low=[SoccerTeam(name=中国队, teamType=COUNTRY, score=10)]}, CLUB={Middle=[SoccerTeam(name=国际米兰, teamType=CLUB, score=60)], High=[SoccerTeam(name=曼城, teamType=CLUB, score=100), SoccerTeam(name=巴塞罗那, teamType=CLUB, score=80)]}}

这里我们使用了一个由双参数版本的Collectors.groupingBy工厂方法创建的收集器,它除了普通的分类函数之外,还可以接受collector类型的第二个参数。所以要进 行二级分组的话,我们可以把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准。通过这种形式,我们还可以继续往下定义任意层级,实现N级分组。

按子组收集数据
在上面的多级分组的例子中,我们通过给groupingBy的第二个收集器参数也传入了groupingBy函数来实现了二组分组,然而,如果我们不需要继续分组,我们也可以传入其他不同的收集器来实现不同的功能。例如,我们可以传入counting函数来统计不同类型的球队的个数。

// 统计不同类型球队的个数
Map<TeamType, Long> countingGroup = teams.stream().collect(
                Collectors.groupingBy(SoccerTeam::getTeamType,
                        Collectors.counting()));// {COUNTRY=1, CLUB=3}

或者我们还可以根据积分分组以后,再通过maxBy函数来找出每个级别积分最高的球队等等。需要注意的是,有的收集器返回的不一定是具体的值(如maxBy函数),而是一个Optional对象。

// 统计不同积分级别积分最高的球队
Map<TeamLevel, Optional<SoccerTeam>> maxScoreGroup = teams.stream()
                .collect(
                        Collectors.groupingBy(team -> {
                            if (team.getScore() > 70) {
                                return TeamLevel.High;
                            } else if (team.getScore() > 30) {
                                return TeamLevel.Middle;
                            }
                            return TeamLevel.Low;
                        }, Collectors.maxBy(Comparator
                                .comparingInt(SoccerTeam::getScore))));
{Middle=Optional[SoccerTeam(name=国际米兰, teamType=CLUB, score=60)], High=Optional[SoccerTeam(name=曼城, teamType=CLUB, score=100)], Low=Optional[SoccerTeam(name=中国队, teamType=COUNTRY, score=10)]}

有时候你可能会觉得返回一个Optional对象不好处理,你也可以使用collectingAndThen方法,将Optional中的对象直接取出来使用。而collectingAndThen接受两个参数,要转换的收集器和转换函数,并返回另一个收集器,因此通过转换函数,你除了可以将Optional中的对象取出来,也可以将它转换成其他任意的对象。

Map<TeamLevel, SoccerTeam> maxScoreTeamGroup = teams.stream().collect(
                Collectors.groupingBy(team -> {
                    if (team.getScore() > 70) {
                        return TeamLevel.High;
                    } else if (team.getScore() > 30) {
                        return TeamLevel.Middle;
                    }
                    return TeamLevel.Low;
                }, Collectors.collectingAndThen(Collectors.maxBy(Comparator
                        .comparingInt(SoccerTeam::getScore)), Optional::get)));
// Optional::get放在这里是安全的,因为reducing收集器永远都不会返回Optional.empty()。

另外还有一个mapping函数,这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加 之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。例如,我们想知道不同类型的球队,都包含哪些积分级别,我们就可以这样来实现:

// 统计不同类型的球队包含哪些积分级别
Map<TeamType, Set<TeamLevel>> typeLevelGroup = teams.stream().collect(
                Collectors.groupingBy(SoccerTeam::getTeamType,
                        Collectors.mapping(team -> {
                            if (team.getScore() > 70) {
                                return TeamLevel.High;
                            } else if (team.getScore() > 30) {
                                return TeamLevel.Middle;
                            }
                            return TeamLevel.Low;
                        }, Collectors.toSet())));
// {COUNTRY=[Low], CLUB=[Middle, High]}

分区
分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函 数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它最多可以 分为两组——true是一组,false是一组。

List<Integer> nums = Lists.newArrayList(13454621);

// 奇偶数分区
System.out.println(nums.stream().collect(
                Collectors.partitioningBy((Integer n) -> n % 2 == 1)));// {false=[4, 4, 6, 2], true=[1, 3, 5, 1]}

还可以像分组函数一样,传入第二个收集器,完成更复杂的操作。例如,找出奇偶数中最大的数字。

// 找出奇偶数中最大的数
System.out.println(nums.stream().collect(
                Collectors.partitioningBy((Integer n) -> n % 2 == 1,
                        Collectors.collectingAndThen(Collectors
                                .maxBy(Comparator
                                        .comparingInt((Integer n) -> n)),
                                Optional::get))));// {false=6, true=5}

自定义收集器
Collector接口包含了一系列方法,为实现具体的归约操作(即收集器)提供了范本。除了我们上面已经看过的这些Collector接口中的实现,例如toList或groupingBy。你还可实现Collector接口提供自己的实现,从而自由地创建自定义归约操作。对这块感觉兴趣的朋友,可以自己看一下相关的参考资料,亲自动手来试试。

并行流
使用流,我们还可以很容易将一段代码并行处理,并且你还无需写任何多线程代码。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让它们都忙起来。
通过对顺序流调用parallel方法,就可以将它转换成一个并行流。

// 顺序流
Stream.iterate(1L, i -> i + 1).limit(100).reduce(0L, Long::sum);

// 并行流
Stream.iterate(1L, i -> i + 1).limit(100).parallel()
                .reduce(0L, Long::sum);

相对的,你只需要对并行流调用sequential方法就可以把它变成顺序流。并且你还可以将这两个方法结合起来使用,从而可以更细化地控制在遍历流时哪些操作要并行执行,哪些要顺序执行。

// 合并使用
Stream.iterate(1L, i -> i + 1).limit(100).parallel()
                .filter(i -> i % 2 == 1).sequential().map(i -> i * i)
                .parallel().reduce((a, b) -> a + b);

需要注意的是,使用并行流并不一定就比顺序流性能要好,因为并行执行就会有多线程之间切换的开销,并且还会有线程安全等其他问题,所以在使用并行流时,一定要特别小心。

结束语
我对Java 8中流的介绍主要就是这么多了,还有很多细节性的问题我并没有一一讲到,大家感兴趣的话可以自己找相关资料来研究研究。相信我,一旦你习惯使用流来处理之后,就会慢慢忘记循环的存在,更不会想再次回到手动迭代的时代。你还在等什么,赶紧用起来吧。

本文案例源码:

https://github.com/lanheixingkong/HelloJava8

参考资料

  • 《Java 8实战》

PS:最后还想再吐槽一下这几大音乐商的版权问题,各自有各自的曲库,弄得自己想插入一首喜欢的歌曲都不行,只能找一个含有几十秒广告的视频放在这里了,无语ing...

也许会落空

也许会普通

也许这庸庸碌碌的黑白世界你不懂

生命中所有的路口

绝不是尽头

别怕 让我留在你身边

都陪你度过

时间一直走 没有尽头 只有路口...


0条留言

留言