流上的函数式操作

1,087 阅读8分钟

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。想必大家对 Steam 再熟悉不过了,但是说到 Java 中的 Stream 呢?今天给大家分享必备小知识是集合操作利器、代码简化杀手之流式编程。

流式操作

What

Stream 即意为 “流”,JDK8Stream 使用的是函数式编程模式,可以被用来对集合进行链状流式的操作。

例如:

    // 给每个用户涨 10000 工资
    users.stream()
                .peek(user -> user.setSalary(user.getSalary() + 10000))
                .forEach(System.out::println);

这种代码风格就称之为 Stream 的流式操作,结合 Lambda 表达式,代码看起来很简洁且优雅 (^o^)/~

Why

  1. 现在很多程序都离不开集合这种常用的数据结构,但是在集合处理方面内置的方法并不能满足个性化需求,Stream 流上的函数式操作应运而生。

  2. 从上述举例中也能窥出一二,使用 Stream 流式操作能够很高效地处理包含大量数据的集合,代替了传统的 for 或者 增强 for 循环的写法,并且使用链式编程使得代码更为简洁优雅。

  3. Stream API 使用了 Fork/Join 框架充分利用了自身机器的多核架构,让流管道以并行的方式运行,即并行流,极大优化处理性能,实现并发编程。

How

如何体验流上的函数式操作呢?首先,你的头脑中要有一个概念,流是数据管道,是用于操作数据源所生成的元素序列。

OK,到此你已经掌握了六七层,你不信?

00D49F65.jpg

别急,且听我慢慢道来……

首先,流式数据管道,你操作一个集合,用 Jio 想你也知道集合不是管道,它是一种存储数据的数据结构,那如何将它变成可以去操作的管道呢?这就涉及到 流的创建 了 ☟

创建 Stream 流常用的几种方式:

Stream 类中创建
  • Stream.of()

语法: image.png

示例代码:

    // 将 value 放入数据处理的管道当中,value 类型为 T
    Stream.of("java", "js", 1, 3.0f, 12L).forEach(System.out::println);
  • Stream.generate()

语法: image.png

示例代码:

    // 生成 5 个 0-1 之间的随机数,它们被放到流中并被逐一打印
    Stream.generate(Math::random).limit(5).forEach(System.out::println);
  • Stream.empty()

语法:

image.png

示例代码:

    // 创建一个空流
    Stream<Object> empty = Stream.empty();
  • Stream.builder()

语法:

image.png

示例代码:

    // 使用 Stream 中构造者模式,往流中添加元素
    Stream.builder()
            .add("java")
            .add("javascript")
            .add("html")
            .add("css").build()
            .forEach(System.out::println);
Arrays 数组工具类中创建
  • Arrays.stream()

语法:

image.png

示例代码:

    // 创建一个整型数组,然后将其变为流进行操作
    Arrays.stream(new int[] {1, 3, 5, 7}).forEach(System.out::println);
Collection 集合接口中默认方法创建
  • Collection.stream() 顺序流

语法:

image.png

示例代码:

    // adults 是一个存储对象的集合,将其变为顺序流(串行流)然后打印输出
    adults.stream().forEach(System.out::println);
  • Collection.parallelStream() 并行流

语法:

image.png

示例代码:

    // adults 是一个存储对象的集合,将其变为并行流然后打印输出
    adults.parallelStream().forEach(System.out::println);

区别:

image.png

stream() 是转化为顺序流,它使用的是主线程,是单线程的;但是 parallelStream() 是转化为并行流,它是多个线程同时运行的。因此,前一个是按顺序输出的,而后一个则显得无序。

操作流的常用 API

到此,我们知道了如何去创建一个流,创建好的流我们需要操作它,那这就涉及到对流的 中间操作 了 ☟

为了不显俗套,这里以问题的方式一一呈现,这样的目的在于更有使用场景的代入!

首先,给出操作数据源:

    private static List<User> users = new ArrayList<>();

    private static List<Integer> nums = Arrays.asList(12,23,52,25,21,67,12);

    static {
        users.add(new User(19, "张三", "男", 19, 3000));
        users.add(new User(20, "李四", "女", 15, 2350));
        users.add(new User(42, "王麻子", "男", 29, 4000));
        users.add(new User(23, "李亮", "女", 39, 2360));
        users.add(new User(51, "张亮", "男", 24, 5220));
        users.add(new User(27, "李梁", "男", 22, 3333));
        users.add(new User(25, "王甜", "女", 11, 2135));
        users.add(new User(42, "石天苟", "女", 16, 4520));
    }
  1. nums 集合中平均数,如果平均数存在则输出。
    OptionalDouble average = nums.stream()
                                          .mapToInt(Integer::intValue)
                                          .average();
    // OptionalDouble average = nums.stream().mapToInt(i -> i).average(); 同上
    
    average.ifPresent(System.out::println);

解析:首先,将 nums 变成可以操作的 stream 流,然后使用中间操作方法 mapToInt() 将流中的每个元素映射成 Integer 类型,最后使用 average() 方法求这些整型元素的平均数,返回的结果为 OptionalDouble 对象,这个对象可以用来判空,只需调用它的 ifPresent() 方法,字面意思,如果存在值则执行括号中的语句。

  1. nums 集合去重后倒序排列,返回这个新的集合。
    List<Integer> newCollect = nums.stream()
                                        .distinct()
                                        .sorted(Comparator.reverseOrder())
                                        .collect(Collectors.toList());
    System.out.println(newCollect);

解析:将 nums 集合变成一个流之后,使用 distinct() 方法去除重复的元素,然后使用 sorted() 方法排序,不带参数时,默认升序,如若需要降序,传入比较器中的一个静态方法 reverseOrder(),到这一步流并没有转化为集合,需要使用 collect(Collectors.toList()) 将其最终转换为一个新的集合。

  1. 查询薪资最高的员工姓名及其薪资多少。
    Optional<User> highestSalary = users.stream()
                                  .max(Comparator.comparingInt(User::getSalary));
    highestSalary.ifPresent( user -> 
            System.out.println("工资最高的用户:" + user.getName() + " ,
            工资为:" + user.getSalary()));

解析:将 User 对象集合变成一个可以操作的流管道,通过 Comparator.comparingInt(User::getSalary) 规定比较的属性以及属性的类型,然后把它放到 max() 方法中作为参数去找到最大薪资的员工,此时,返回的结果为 Optional<User> 对象,这个对象可以用来判空,只需调用它的 ifPresent() 方法,字面意思,如果存在值则执行括号中的语句。

  1. 将存储 User 集合中的 Salary 全部取出来组装成一个薪资集合,然后求薪资的总额。
List<Integer> salarys = users.stream()
                                    .map(User::getSalary)
                                    .collect(Collectors.toList());
Integer sum = salarys.stream()
                    .reduce(0, (result, currentSalary) -> result + currentSalary);
System.out.println("薪资总额:" + sum);

解析:首先,将 User 对象集合变成一个可以操作的流管道,使用 map() 方法映射员工的薪资,然后将流转化成 List 集合,然后再对这个薪资集合做同样的操作,但不是使用映射方法而是使用 reduce() 归约,也叫做缩减,该方法指定一个容器,专门用来接收从右往左缩减操作后的结果,第一个参数规定了容器的初始值,第二个参数使用了函数式接口,规定了容器变量名以及当前元素,函数的实现则是缩减操作,规定要做什么。这里只是简单的累加,所有把当前值累加到容器中即可,返回的结果就是薪资总额了。

  1. 找出所有用户名,在用户名前缀为 user_ ,用 对每一个用户进行分割,拼接后的结果最前面和最后面随意点缀任意字符。
    String userNames = users.stream()
            .map(user -> "user_"+user.getName())
            .collect(Collectors.joining("、", "拼接后的结果:", "-> 小尾巴"));
            
// Run =>  拼接后的结果:user_张三、user_李四、user_王麻子、user_李亮、user_张亮、user_李梁、user_王甜、user_石天苟-> 小尾巴

解析:不赘述了,这里使用 map() 映射到每个用户的用户名,然后用 Collectors 类中静态方法 joining(),给用户名添加 delimiter 分隔符,最后结果添加上prefix 前缀,suffix 后缀。

  1. 将工资大于 3000 的用户进行分组
    Map<Boolean, List<User>> groupResult = users.stream()
                .collect(Collectors.partitioningBy(user -> user.getSalary() > 3000));
    // 工资小于等于 3000 的用户集合
    List<User> group1 = groupResult.get(false);
    // 工资大于 3000 的用户集合
    List<User> group2 = groupResult.get(true);

    System.out.println("工资小于等于 3000 的用户集合:" + group1);
    System.out.println("工资大于 3000 的用户集合:" + group2);

解析:这里主要就是使用了 Collectors 类中静态方法 partitioningBy(),这个方法的参数是分组的依据,如果满足分组的依据,那么返回键就是 true,否则为 false,值的话就是分组成员了。

  1. 将所有员工按照男女进行分组。
    Map<String, List<User>> groupResult = users.stream()
                             .collect(Collectors.groupingBy(User::getSex));
    // 男子组
    List<User> males = groupResult.get("男");
    // 女子组
    List<User> female = groupResult.get("女");

    System.out.println("男子组:" + males);
    System.out.println("女子组:" + female);

解析:是不是有一个问题:为什么上面可以使用 Collectors.partitioningBy() 分组,而这里却不用呢?因为 partitioningBy() 方法参数只认 Predicate 断言,简单点说,就是真或假的表达式!仍要坚持使用这个方式也不是不可以,就是比较麻烦,需要写一个 equal() 判断;但是使用 Collectors.groupingBy() 却可以不用,一般情况下,性别只有男或女,所以只需根据属性值判断是否属于同一组中的。

  1. 返回一个 Map 键值对,键为员工的姓名,值为员工信息。
    Map<String, User> map = users.stream().collect(Collectors.toMap(User::getName, user -> user));
    System.out.println(map);

解析:这里将流中的操作的对象映射到了 Map 集合上,员工名对应 Mapkey,员工详细信息对应的是 Mapvalue,使用 Collectors.toMap() 实现。

  1. 筛选出年龄在 10-25 岁之间且薪资在 1500 以上的员工。
    // 通过 Predicate 断言 test
    Predicate<User> userPredicate1 = user -> user.getAge() < 25;
    Predicate<User> userPredicate2 = user -> user.getAge() > 10;
    Predicate<User> userPredicate3 = user -> user.getSalary() > 1500;

    List<User> userList =
            list1.stream()
                    .filter(userPredicate1)
                    .filter(userPredicate2)
                    .filter(userPredicate3)
                    .collect(Collectors.toList());
                    
    // 多个 filter 可以变为一个
    /*
    List<User> collect1 =
            list1.stream()
                    .filter(userPredicate1.and(userPredicate2).and(userPredicate3))
                    .collect(Collectors.toList());
   */

解析:这个案例中使用 filter() 方法对管道中的数据进行筛选,方法的参数为 Predicate 断言,满足断言的留下,不符合条件的剔除,最后返回的结果经过转换成集合。

末端结果返回

末端返回总是在中间操作过后,为了终止流向后传递,返回最终的结果形态,可以是ListOptionalMapStringInteger 等等,在上述问题案例中均有体现,这里就不再过多赘述了……

总结

为了加深大家记忆,以图高度概括为:

流上的函数式操作.png

了解更多

如果想了解更多相关用法,且英语水平不错的可以参考 官方文档

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。