前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 8中集合优雅快速的处理方式

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

作者头像
攻城狮的那点事
发布2019-10-24 03:22:51
3K0
发布2019-10-24 03:22:51
举报

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

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

什么是Stream

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

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

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

集合中流的操作

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

代码语言:javascript
复制
Stream<String> stream = list.stream().map(User::getName).sorted().limit(10);         
List<String> newList = stream.collect(toList());
List<String> newList2 = stream.collect(toList());

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

代码语言:javascript
复制
java.lang.IllegalStateException: stream has already been operated upon or closed

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

下面我们开始实战

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

代码语言:javascript
复制
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()

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

代码语言:javascript
复制
List list = new ArrayList();
// return Stream<E>
list.stream();

2,filter(T -> boolean)

保留 boolean 为 true 的元素。

代码语言:javascript
复制
保留年龄大于的 user 元素
list = list.stream()
            .filter(person -> person.getAge() >= 20)
            .collect(toList());

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

3,distinct()

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

代码语言:javascript
复制
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 接口。

代码语言:javascript
复制
根据年龄大小来比较:
list = list.stream()
           .sorted((p1, p2) -> p1.getAge() - p2.getAge())
           .collect(toList());

当然这个可以简化为

代码语言:javascript
复制
list = list.stream()
           .sorted(Comparator.comparingInt(User::getAge))
           .collect(toList());

5,limit(long n)

返回前 n 个元素

代码语言:javascript
复制
list = list.stream()
            .limit(2)
            .collect(toList());

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

6,skip(long n)

去除前 n 个元素。

代码语言:javascript
复制
list = list.stream()
            .skip(2)
            .collect(toList());

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

注意:

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

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

7,map(T -> R)

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

代码语言:javascript
复制
List<String> list01 = list.stream().map(User::getName).collect(toList());

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

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

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

代码语言:javascript
复制
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 条件。

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

10,allMatch(T -> boolean)

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

代码语言:javascript
复制
是否所有user对象的 age 都大于18;
boolean b = list.stream().allMatch(user -> user.getAge() > 18);

11,noneMatch(T -> boolean)

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

代码语言:javascript
复制
是否有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)

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

代码语言:javascript
复制
计算年龄总和:
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 即将两值相加产生一个新值。

同样地:

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

当然也可以

代码语言:javascript
复制
Optional<Integer> sum = list.stream().map(User::getAge).reduce(Integer::sum);

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

14,count()

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

代码语言:javascript
复制
long num = list.stream().count();

15. forEach()

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

代码语言:javascript
复制
打印出各个元素:
list.stream().forEach(System.out::println);

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

代码语言:javascript
复制
向数据库插入新元素:
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 的数值流。

代码语言:javascript
复制
求 1 到 100 的数值总和:
IntStream intStream = IntStream.rangeClosed(1, 100);int sum = intStream.sum();

构建流

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

1,值创建流

代码语言:javascript
复制
生成字符串流
Stream<String> stream = Stream.of("tom", "lily", "hahahaha");
  • Stream.empty() : 生成空流

2,数组创建流

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

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

打印 2 ,3

3,文件生成流

代码语言:javascript
复制
Stream<String> stream = Files.lines(Paths.get("test.txt"));

4,函数生成流

有如下方法:

  • iterate :依次对每个新生成的值应用函数
  • generate :接受一个函数,生成一个新的值
代码语言:javascript
复制
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
代码语言:javascript
复制
List newlist = list.stream.collect(toList());
代码语言:javascript
复制
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 平均数

代码语言:javascript
复制
Double average = list.stream().collect(averagingInt(User::getAge));
代码语言:javascript
复制
可写成:OptionalDouble average = list.stream().mapToInt(User::getAge).average();

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

4),summarizingInt,summarizingLong,summarizingDouble

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

代码语言:javascript
复制
IntSummaryStatistics l = list.stream().collect(summarizingInt(User::getAge));

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

获取最值

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

代码语言:javascript
复制
Optional<Person> optional = list.stream().collect(maxBy(comparing(User::getAge)));

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

代码语言:javascript
复制
Optional<User> optional = list.stream().max(comparing(User::getAge));

joining连接字符串

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

代码语言:javascript
复制
String s = list.stream().map(User::getName).collect(joining());

结果:LucyLeonTom
代码语言:javascript
复制
String s = list.stream().map(User::getName).collect(joining(","));

结果:Lucy,Leon,Tom

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

代码语言:javascript
复制
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 类型。

代码语言:javascript
复制
Map<Integer, List<User>> map = list.stream().collect(groupingBy(User::getAge));

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

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

多级分组

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

代码语言:javascript
复制
Map<Integer, Map<T, List<User>>> map = list.stream().collect(groupingBy(User::getAge, groupingBy(...)));

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

按组收集数据

代码语言:javascript
复制
Map<Integer, Integer> map = list.stream().collect(groupingBy(User::getAge, summingInt(User::getAge)));

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

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

代码语言:javascript
复制
groupingBy(User::getAge)

等同于:

代码语言:javascript
复制
groupingBy(User::getAge, toList())

partitioningBy 分区

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

代码语言:javascript
复制
根据年龄是否小于等于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 的出现意义还是非常大的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 攻城狮的那点事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是Stream
  • 集合中流的操作
    • 1,stream()
      • 2,filter(T -> boolean)
        • 3,distinct()
          • 4,sorted() / sorted((T, T) -> int)
            • 5,limit(long n)
              • 6,skip(long n)
                • 7,map(T -> R)
                  • 8,flatMap(T -> Stream<R>)
                    • 9,anyMatch(T -> boolean)
                      • 10,allMatch(T -> boolean)
                        • 11,noneMatch(T -> boolean)
                          • 12,findAny() 和 findFirst()
                            • 13,reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)
                              • 14,count()
                                • 15. forEach()
                                  • 流转换为数值流:
                              • 数值流
                                • 数值流方法:
                                  • 数值范围:
                                  • 构建流
                                    • 1,值创建流
                                      • 2,数组创建流
                                        • 3,文件生成流
                                          • 4,函数生成流
                                          • Collect收集数据
                                          • 数据的汇总
                                            • 1),counting() 用于计算总和。
                                              • 2),summingInt ,summingLong ,summingDouble
                                                • 3),averagingInt,averagingLong,averagingDouble 平均数
                                                  • 4),summarizingInt,summarizingLong,summarizingDouble
                                                  • 获取最值
                                                  • joining连接字符串
                                                  • groupingBy 分组
                                                    • 多级分组
                                                      • 按组收集数据
                                                      • partitioningBy 分区
                                                      • 效率
                                                      相关产品与服务
                                                      容器服务
                                                      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                                                      领券
                                                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档