专栏首页攻城狮的那点事Java 8中集合优雅快速的处理方式

Java 8中集合优雅快速的处理方式

相信现在大多数的伙伴们,都在使用Java 8了,而 Java 8相比以前的版本,是作出了革命性的改变。Java8的特性大致可总结为,开发速度更快,代码更少,增加了Lambda,强大的Stream API,便于并行,最大化减少空指针异常。

本文主要讲解Java 8的Stream,Stream 是用函数式编程方式在集合类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作,开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找、遍历、过滤以及常见计算等。

什么是Stream

Stream 中文称为 “流”,通过将集合转换为这么一种叫 “流” 的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。换句话说,你只需要告诉流你的要求,流便会在背后自行根据要求对元素进行处理,而你只需要 “坐享其成”。

整个流操作就是一条流水线,将元素放在流水线上一个个地进行处理。很多流操作本身就会返回一个流,所以多个操作可以直接连接起来,下面是一条 Stream 操作的代码:

JDK8以前,进行这一系列操作,你需要做个迭代器或者 foreach 循环,然后遍历,一步步地亲力亲为地去完成这些操作;但是如果使用流,你便可以直接声明式地下指令,流会帮你完成这些操作。就像你想查数据库User表中的某个用户,只要知道id即可通过查询语句获取,无需你去遍历查寻整个数据库。

集合中流的操作

首先,大家应该知道流的基本特性吧,那就是流是一次性的,和迭代器类似,只能迭代一次。

Stream<String> stream = list.stream().map(User::getName).sorted().limit(10);         
List<String> newList = stream.collect(toList());
List<String> newList2 = stream.collect(toList());

大家看看上面的代码,能运行通过吗?实际运行结果如下:

java.lang.IllegalStateException: stream has already been operated upon or closed

这是因为在上面的代码中,第二行已经使用了流,流已被消费掉了。所以第三行再获取就会报错。

下面我们开始实战

首先我们先创建一个 User泛型的 List,实体User包含name,age。

List<User> list = new ArrayList<>();
list.add(new User("Lucy", 25));
list.add(new User("Leon", 21));
list.add(new User("Tom", 18));

1,stream()

最常用到的方法,将集合转换为流。

List list = new ArrayList();
// return Stream<E>
list.stream();

2,filter(T -> boolean)

保留 boolean 为 true 的元素。

保留年龄大于的 user 元素
list = list.stream()
            .filter(person -> person.getAge() >= 20)
            .collect(toList());

打印输出 [User{name='Lucy', age=25}, User{name='Leon', age=21}]

3,distinct()

去除重复元素,这个方法是通过类的 equals 方法来判断两个元素是否相等的。

List<User> list = new ArrayList<>();list.add(new User("Lucy", 25));
list.add(new User("Leon", 21));
list.add(new User("Tom", 18));
list.add(new User("Tom", 18));list = list.stream()
            .distinct()
            .collect(toList());

输出[User{name='Lucy', age=25}, User{name='Leon', age=21}, User{name='Tom', age=18}]

4,sorted() / sorted((T, T) -> int)

如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Stream<Integer>。反之, 需要调用 sorted((T, T) -> int) 实现 Comparator 接口。

根据年龄大小来比较:
list = list.stream()
           .sorted((p1, p2) -> p1.getAge() - p2.getAge())
           .collect(toList());

当然这个可以简化为

list = list.stream()
           .sorted(Comparator.comparingInt(User::getAge))
           .collect(toList());

5,limit(long n)

返回前 n 个元素

list = list.stream()
            .limit(2)
            .collect(toList());

打印输出 [User{name='Lucy', age=25}, User{name='Leon', age=21}]

6,skip(long n)

去除前 n 个元素。

list = list.stream()
            .skip(2)
            .collect(toList());

打印输出 [User{name='Lucy', age=25}]

注意:

  • 用在 limit(n) 前面时,先去除前 m 个元素再返回剩余元素的前 n 个元素。
  • limit(n) 用在 skip(m) 前面时,先返回前 n 个元素再在剩余的 n 个元素中去除 m 个元素。
list = list.stream()
            .limit(2)
            .skip(1)
            .collect(toList());

打印输出 [User{name='Leon', age=21}]

7,map(T -> R)

将流中的每一个元素 T 映射为 R(类似类型转换)

List<String> list01 = list.stream().map(User::getName).collect(toList());

list01里面的元素为 list 中每一个 User对象的 name 变量。

8,flatMap(T -> Stream<R>)

将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流

List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");

list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());

上面例子中,我们的目的是把 List 中每个字符串元素以" "分割开,变成一个新的 List<String>。 首先 map 方法分割每个字符串元素,但此时流的类型为 Stream<String[ ]>,因为 split 方法返回的是 String[ ] 类型;所以我们需要使用 flatMap 方法,先使用Arrays::stream将每个 String[ ] 元素变成一个 Stream<String> 流,然后 flatMap 会将每一个流连接成为一个流,最终返回我们需要的 Stream<String>。

9,anyMatch(T -> boolean)

流中是否有一个元素匹配给定的 T -> boolean 条件。

是否存在一个 user对象的 age 等于 20:
boolean b = list.stream().anyMatch(user -> user.getAge() == 20);

10,allMatch(T -> boolean)

流中是否所有元素都匹配给定的 T -> boolean 条件。

是否所有user对象的 age 都大于18;
boolean b = list.stream().allMatch(user -> user.getAge() > 18);

11,noneMatch(T -> boolean)

流中是否没有元素匹配给定的 T -> boolean 条件。

是否有user对象的 age 小于18;
boolean b = list.stream().noneMatch(user -> user.getAge() < 18);

12,findAny() 和 findFirst()

  • findAny():找到其中一个元素 (使用 stream() 时找到的是第一个元素;使用 parallelStream() 并行时找到的是其中一个元素)。
  • findFirst():找到第一个元素。

值得注意的是,这两个方法返回的是一个 Optional<T> 对象,它是一个容器类,能代表一个值存在或不存在,这个后面会讲到。

13,reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)

用于组合流中的元素,如求和,求积,求最大值等。

计算年龄总和:
int sum = list.stream().map(User::getAge).reduce(0, (a, b) -> a + b);
与之相同:
int sum = list.stream().map(User::getAge).reduce(0, Integer::sum);

其中,reduce 第一个参数 0 代表起始值为 0,lambda (a, b) -> a + b 即将两值相加产生一个新值。

同样地:

计算年龄总乘积:
int sum = list.stream().map(User::getAge).reduce(1, (a, b) -> a * b);

当然也可以

Optional<Integer> sum = list.stream().map(User::getAge).reduce(Integer::sum);

即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型。

14,count()

返回流中元素个数,结果为 long 类型。

long num = list.stream().count();

15. forEach()

返回结果为 void,很明显我们可以通过它来干什么了,比方说:

打印出各个元素:
list.stream().forEach(System.out::println);

再比如说 MyBatis 里面访问数据库的 mapper 方法:

向数据库插入新元素:
list.stream().forEach(UserMapper::insertUser);

数值流

前面介绍的如list.stream().map(User::getAge).reduce(0, Integer::sum); 计算总和时暗含了装箱成本,map(User::getAge) 方法过后流变成了 Stream<Integer> 类型,而每个 Integer 都要拆箱成一个原始类型再进行 sum 方法求和,这样大大影响了效率。

针对此问题,Java 8引入了数值流IntStream,DoubleStream,LongStream,这种流中的元素都是原始数据类型,分别是 int,double,long。

流转换为数值流:

  • mapToInt(T -> int) : return IntStream
  • mapToDouble(T -> double) : return DoubleStream
  • mapToLong(T -> long) : return LongStream

IntStream intStream = list.stream().mapToInt(User::getAge);

数值流方法:

  • sum()
  • max()
  • min()
  • average() 等...

数值范围:

IntStream 与 LongStream 拥有 range 和 rangeClosed 方法用于数值范围处理

  • IntStream :rangeClosed(int, int) / range(int, int)
  • LongStream :rangeClosed(long, long) / range(long, long)

这两个方法的区别在于一个是闭区间,一个是半开半闭区间:

  • rangeClosed(1, 100) :[1, 100]
  • range(1, 100) :[1, 100)

我们可以利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的数值流。

求 1 到 100 的数值总和:
IntStream intStream = IntStream.rangeClosed(1, 100);int sum = intStream.sum();

构建流

之前我们得到一个流是通过一个原始数据源转换而来,其实我们还可以直接构建得到流。

1,值创建流

生成字符串流
Stream<String> stream = Stream.of("tom", "lily", "hahahaha");
  • Stream.empty() : 生成空流

2,数组创建流

根据参数的数组类型创建对应的流:

  • Arrays.stream(T[ ])
  • Arrays.stream(int[ ])
  • Arrays.stream(double[ ])
  • Arrays.stream(long[ ])
只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);

打印 2 ,3

3,文件生成流

Stream<String> stream = Files.lines(Paths.get("test.txt"));

4,函数生成流

有如下方法:

  • iterate :依次对每个新生成的值应用函数
  • generate :接受一个函数,生成一个新的值
Stream.iterate(0, n -> n + 1)
生成流,第一个为0,后面的依次加1

Stream.generate(Math :: random)
生成流,为 0 到 1 的随机双精度数

Stream.generate(() -> 1)
生成流,元素全为 1

Collect收集数据

Collect 方法作为终端操作,接受的是一个 Collector 接口参数,能对数据进行一些收集归总操作。

收集最常用的方法,是把流中所有元素收集到一个 List, Set 或 Collection 中。如下:

  • toList
  • toSet
  • toCollection
  • toMap
List newlist = list.stream.collect(toList());
Map<Integer, User> map = list.stream().collect(toMap(User::getAge, p -> p));
//注意:如果Map的Key重复了,会报错

数据的汇总

1),counting() 用于计算总和。

long l = list.stream().collect(counting());

或:long l = list.stream().count();//推荐使用

2),summingInt ,summingLong ,summingDouble

summing,没错,也是计算总和,不过这里需要一个函数参数

int sum = list.stream().collect(summingInt(User::getAge));

可简化为(推荐):int sum = list.stream().mapToInt(User::getAge).sum();

3),averagingInt,averagingLong,averagingDouble 平均数

Double average = list.stream().collect(averagingInt(User::getAge));
可写成:OptionalDouble average = list.stream().mapToInt(User::getAge).average();

注意:这两种返回的值是不同类型的。

4),summarizingInt,summarizingLong,summarizingDouble

这三个方法比较特殊,比如 summarizingInt 会返回 IntSummaryStatistics 类型

IntSummaryStatistics l = list.stream().collect(summarizingInt(User::getAge));

IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,可以通过下面这些方法获得相应的数据。

获取最值

通过maxBy,minBy 两个方法,需要一个 Comparator 接口作为参数

Optional<Person> optional = list.stream().collect(maxBy(comparing(User::getAge)));

我们也可以直接使用 max 方法获得同样的结果

Optional<User> optional = list.stream().max(comparing(User::getAge));

joining连接字符串

也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder。

String s = list.stream().map(User::getName).collect(joining());

结果:LucyLeonTom
String s = list.stream().map(User::getName).collect(joining(","));

结果:Lucy,Leon,Tom

joining 还有一个比较特别的重载方法:

String s = list.stream().map(User::getName).collect(joining(" and ", "Today ", " play games."));

结果:Today Lucy and Leon and Tom play games.

即 Today 放开头,play games. 放结尾,and 在中间连接各个字符串。

groupingBy 分组

groupingBy 用于将数据分组,最终返回一个 Map 类型。

Map<Integer, List<User>> map = list.stream().collect(groupingBy(User::getAge));

例子中我们按照年龄 age 分组,每一个 User 对象中年龄相同的归为一组。

另外可以看出,User::getAge 决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List<User> 类型)。

多级分组

groupingBy 可以接受一个第二参数实现多级分组:

Map<Integer, Map<T, List<User>>> map = list.stream().collect(groupingBy(User::getAge, groupingBy(...)));

其中返回的 Map 键为 Integer 类型,值为 Map<T, List<Person>> 类型,即参数中 groupBy(...) 返回的类型

按组收集数据

Map<Integer, Integer> map = list.stream().collect(groupingBy(User::getAge, summingInt(User::getAge)));

该例子中,我们通过年龄进行分组,然后 summingInt(User::getAge)) 分别计算每一组的年龄总和(Integer),最终返回一个 Map<Integer, Integer>。

根据这个方法,我们可以知道,前面我们写的:

groupingBy(User::getAge)

等同于:

groupingBy(User::getAge, toList())

partitioningBy 分区

分区与分组的区别在于,分区是按照 true 和 false 来分的,因此partitioningBy 接受的参数的 lambda 也是 T -> boolean

根据年龄是否小于等于20来分区
Map<Boolean, List<User>> map = list.stream()
                                     .collect(partitioningBy(p -> p.getAge() <= 20));

打印输出
{
    false=[User{name='Lucy', age=25}, User{name='Leon', age=21}],     true=[User{name='Tom', age=18}]}

同样地 partitioningBy 也可以添加一个收集器作为第二参数,进行类似 groupBy 的多重分区等等操作。

效率

最后,我们再来谈谈效率问题,很多人可能听说过有关 Stream 效率低下的问题。其实,对于一些简单的操作,比如单纯的遍历,查找最值等等,Stream 的性能的确会低于传统的循环或者迭代器实现,甚至会低很多。

但是对于复杂的操作,比如一些复杂的对象归约,Stream 的性能是可以和手动实现的性能匹敌的,在某些情况下使用并行流,效率可能还远超手动实现。好钢用在刀刃上,在适合的场景下使用,才能发挥其最大的用处。

函数式接口的出现主要是为了提高编码开发效率以及增强代码可读性;与此同时,在实际的开发中,并非总是要求非常高的性能,因此 Stream 与 lambda 的出现意义还是非常大的。

本文分享自微信公众号 - 攻城狮的那点事(gh_e40249fc5212)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 使用JMeter做MongoDB性能测试

    对大多数应用环境来说,数据库是一个关键要素。如何存储数据以及在哪里存储数据,对整个系统的性能会产生巨大影响。因此,在做开发之前,数据库的选择肯定是最重要的决定之...

    MongoDB中文社区
  • 利用nohup后台运行jar文件包程序

    java -jar XXX.jar 特点:当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出

    Java架构师历程
  • 基于设备指纹零感验证系统

    作者: 我是小三 博客: http://www.cnblogs.com/2014asm/ 由于时间和水平有限,本文会存在诸多不足,希望得到您的及时反馈与指正,多...

    我是小三
  • 07.Django学习之model进阶

    使用Python 的切片语法来限制查询集记录的数目 。它等同于SQL 的LIMIT 和OFFSET 子句。

    changxin7
  • Weblogic反序列化历史漏洞全汇总

    序列化是让Java对象脱离Java运行环境的一种手段,可以有效的实现多平台之间的通信、对象持久化存储。

    Jayway
  • 基于nGrinder下的web网站性能测试

    nGrinder 看名字估计很多人就猜到跟Grinder有关系。nGrinder是韩国一家公司居于Grinder二次开发的一个性能平台。nGrinder具有 开...

    用户6367961
  • 一起学Rust-实战leetcode(三)

    之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"。

    MikeLoveRust
  • itext根据模板生成pdf(支持分页)

    // 利用模板生成pdf public static void pdfout(Map<String,Object> o,String newPDFPa...

    用户4478423
  • Spring MVC适配器模式实践之HandlerAdapter源码分析【享学Spring MVC】

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    BAT的乌托邦
  • Springboot集成swagger2生成接口文档

    原文出处:https://www.cnblogs.com/jstarseven/p/11509884.html 作者:jstarseven 码字挺辛苦的....

    大道七哥

扫码关注云+社区

领取腾讯云代金券