Java 8 - Stream流骚操作解读

文章目录


Java 8 -  Stream流骚操作解读


分类

java.util.stream.Stream 中的 Stream 接口定义了许多操作。

Java 8 -  Stream流骚操作解读

我们来看个例子

Java 8 -  Stream流骚操作解读

可以分为两大类操作

  • filter 、 map 和 limit 可以连成一条流水线

  • collect 触发流水线执行并关闭它

可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作

Java 8 -  Stream流骚操作解读


中间操作

诸如 filter 或 sorted 等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理 。

这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处理的数据

    /**
     * 需求: 输出小于400的Dish的名字 , 按照卡路里从第到高输出
     * @param dishList
     * @return
     */
    public static List<String> getDiskNamesByStream2(List<Dish> dishList) {
        return dishList.stream()
                .filter(dish -> {
                    System.out.println("filtering:" + dish.getName());
                    return dish.getCalories() <400 ;
                })
                .map(dish -> {
                    System.out.println("mapping:" + dish.getName());
                    return dish.getName();
                })
                .limit(3)
                .collect(Collectors.toList());
    }

仅限于自己学习的时候这么写哈

Java 8 -  Stream流骚操作解读

输出

filtering:pork
mapping:pork
filtering:beef
mapping:beef
filtering:chicken
mapping:chicken
[pork, beef, chicken]

这里面有好几种优化利用了流的延迟性质。

  • 第一,尽管很高于300卡路里的数据,但只选出了前三个。 因为 limit 操作和一种称为短路的技巧
  • 第二,尽管 filter 和 map 是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并

终端操作

终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如 List 、 Integer ,甚至 void 。例如,在下面的流水线中, forEach 是一个返回 void 的终端操作,它会对源中的每道菜应用一个Lambda。把 System.out.println 传递给 forEach ,并要求它打印出由 menu 生成的流中的每一个 Dish

 dishList.stream().forEach(System.out::println);

使用Stream流

流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果

流的流水线背后的理念类似于构建器模式。 在构建器模式中有一个调用链用来设置一套配置(对流来说这就是一个中间操作链),接着是调用 built 方法(对流来说就是终端操作)。

列一下之前的代码中我们用到的流操作,当然了不止这些

【中间操作】
Java 8 -  Stream流骚操作解读

【终端操作】

Java 8 -  Stream流骚操作解读

还有很多模式,过滤、切片、查找、匹配、映射和归约可以用来表达复杂的数据处理查询。

Java 8 -  Stream流骚操作解读

来看看其他的,当然了不全哈

Java 8 -  Stream流骚操作解读


筛选和切片

如何选择流中的元素?

用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度.

用谓词筛选 filter

Streams 接口支持 filter 方法 ,该操作会接受一个谓词(一个返回boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流.

需求: 筛选出所有素菜

Java 8 -  Stream流骚操作解读

    /**
     * 需求:  输出所有的素菜
     * @param dishList
     * @return
     */
    public static List<Dish> getVegetarianByStream(List<Dish> dishList){

        return dishList.stream()
                .filter(Dish::isVegetarian)
                .collect(Collectors.toList());
    }

Java 8 -  Stream流骚操作解读


筛选各异的元素 distinct

流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的hashCode 和 equals 方法实现)的流。

需求: 给定一组数据,筛选出列表中所有的偶数,并确保没有重复

    public static void testDistinct(){
        Arrays.asList(1,2,1,3,3,2,4)
                .stream()
                .filter(i -> i%2 ==0)
                .distinct()
                .forEach(System.out::println);
    }

Java 8 -  Stream流骚操作解读


截短流 limit

流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给 limit 。如果流是有序的,则最多会返回前 n 个元素。

需求: 选出热量超过300卡路里的头三道菜

 /**
     * 选出热量超过300卡路里的头三道菜
     * @param dishes
     * @return
     */
    public static List<String> getTop3HighCa(List<Dish> dishes) {
        return dishes.stream()
                .filter(d -> d.getCalories() > 300)
                .limit(3) 
                .collect(Collectors.toList());
    }

展示了 filter 和 limit 的组合。可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。

请注意 limit 也可以用在无序流上,比如源是一个 Set 。这种情况下, limit 的结果不会以任何顺序排列。

Java 8 -  Stream流骚操作解读


跳过元素 skip

流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一 个空流。请注意, limit(n) 和 skip(n) 是互补的

需求: 跳过超过300卡路里的头两道菜,并返回剩下的

    /**
     * 需求: 跳过超过300卡路里的头两道菜,并返回剩下的
     * @param dishes
     * @return
     */
    public static List<Dish> skipTop2Over300Carl(List<Dish> dishes) {
        return dishes.stream().filter(d->d.getCalories()>300)
                .skip(2) 
                .collect(Collectors.toList());
    }

Java 8 -  Stream流骚操作解读


映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过 map 和 flatMap 方法提供了类似的工具。
Java 8 -  Stream流骚操作解读

对流中每一个元素应用函数 map

流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。

举个例子 :

    public static List<String> getMenu(List<Dish> dishes) {
        return dishes.stream()
                .map(Dish::getName)
                .collect(Collectors.toList());
    }

功能: 把方法引用 Dish::getName 传给了 map 方法来提取流中菜肴的名称。

因为 getName 方法返回一个 String ,所以 map 方法输出的流的类型就是 Stream<String>

【再来看个例子 】

给定一个单词列表,想要返回另一个列表,显示每个单词中有几个字母。

怎么做呢?你需要对列表中的每个元素应用一个函数。

这听起来正好该用 map 方法去做!应用的函数应该接受一个单词,并返回其长度。你可以像下面这样,给 map 传递一个方法引用 String::length 来解决这个问题:

    /**
     * 给定一个单词列表,想要返回另一个列表,显示每个单词中有几个字母。
     * @return
     */
    public static List mapping(){
        List<String> list = Arrays.asList("abc","pear","child","artisan");
        return list.stream().map(d->d.length()).collect(Collectors.toList());
    }

使用方法引用优化

  public static List mapping(){
        List<String> list = Arrays.asList("abc","pear","child","artisan");
        return list.stream().map(String::length).collect(Collectors.toList());
    }

那再看个

    /**
     * 需求:  要找出每道菜的名称有多长
     * @param dishes
     * @return
     */
    public static List<Integer> getMenuLength(List<Dish> dishes) {
        return dishes.stream()
                .map(Dish::getName)
                .map(String::length)
                .collect(Collectors.toList());
    }

Java 8 -  Stream流骚操作解读


流的扁平化 flatMap

我们已经看到如何使用 map 方法返回列表中每个单词的长度了。

让我们扩展一下:对于一张单词表 , 如何返回一张列表 , 列出里面各不相同的字符呢?

怎么实现呢?

是不是可以把每个单词映射成一张字符表,然后调用 distinct 来过滤重复字符, 秒啊

Java 8 -  Stream流骚操作解读

public static List<String[]> test() {
        List<String> list = Arrays.asList("hello","world");
        return list.stream().map(t -> t.split(" ")).distinct().collect(Collectors.toList()) ;
    }

(⊙o⊙)… ,不对。。。。

这个方法的问题在于,传递给 map 方法的Lambda为每个单词返回了一个 String[] ( String列表)。因此, map 返回的流实际上是 Stream<String[]> 类型的。 我们真正想要的是用Stream<String> 来表示一个字符流

Java 8 -  Stream流骚操作解读

Java 8 -  Stream流骚操作解读

那这么办呢?

尝试使用map 和 Arrays.stream() 【未解决】

可以用 flatMap 来解决这个问题!让我们一步步看看怎么解决它。

  /**
     * 需求:  对于一张单词表 , 如何返回一张列表 , 列出里面各不相同的字符呢?
     *
     * 错误的写法
     * @return
     */
    public static List<Stream<String>> test2() {
        Stream<String> stream = Arrays.stream(new String[]{"hello", "world"});
        return stream.map(t->t.split(" ")).map(Arrays::stream).distinct().collect(Collectors.toList());
    }

Java 8 -  Stream流骚操作解读

当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的列表(更准确地说是Stream<String>)。的确,你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流。

使用flatMap 【解决】

    /**
     * 需求:  对于一张单词表 , 如何返回一张列表 , 列出里面各不相同的字符呢?
     * <p>
     * 完美
     *
     * @return
     */
    public static List<String> test3() {
        Stream<String> stream = Arrays.stream(new String[]{"hello", "world"});
        return stream.map(t -> t.split(" "))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(Collectors.toList());
        }

Java 8 -  Stream流骚操作解读
使用 flatMap 方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容 。 所有使用 flatMap(Arrays::stream) 时生成的单个流都被合并起来,即扁平化为一个流
Java 8 -  Stream流骚操作解读

一言以蔽之, flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。


查找和匹配

另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。StreamAPI通过 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法提供了这样的工具。


检查谓词中是否至少匹配一个元素 anyMatch

anyMatch 方法可以回答“流中是否有一个元素能匹配给定的谓词”。

比如,你可以用它来看

   /**
     * 需求:  是否包含素菜
  
     */
    public static void isVe(List<Dish> dishes) {
        if (dishes.stream().anyMatch(Dish::isVegetarian)){
            System.out.println("you su cai ");
        }
    }

anyMatch 方法返回一个 boolean ,因此是一个终端操作.


检查谓词中是否匹配所有元素 allMatch

allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。

比如,你可以用它来看看所有热量是否都低于1000卡路里

    /**
     * 需求:  看看所有热量是否都低于1000卡路里
     */
    public static void isHea(List<Dish> dishes) {
        if (dishes.stream().allMatch(d->d.getCalories()<1000)){
            System.out.println("oj8k ");
        }
    }

检查谓词中都不匹配所有元素 noneMatch

和 allMatch 相对的是 noneMatch 。它可以确保流中没有任何元素与给定的谓词匹配。

    /**
     * 需求:  没有任何一个卡路里超过1000
     */
    public static void isHeass(List<Dish> dishes) {
        if (dishes.stream().noneMatch(d->d.getCalories()>=1000)){
            System.out.println("muyou ");
        }
    }

anyMatch 、 allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中 && 和 || 运算符短路在流中的版本

Java 8 -  Stream流骚操作解读


什么是短路求值

有些操作不需要处理整个流就能得到结果。例如,你需要对一个用 and 连起来的大布尔表达式进行求职,不管表达式有多长,只要有一个是false,那么就可以推断出整个表达式是false, 无需计算整个表达式,这就是短路

对于流而言,某些操作 (例如 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny )不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了

同样的,limit也是一个短路操作。 它只需要创建一个给?大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时?,这种操作就有用了:它们可以把无限流变成有限流。


查找元素 findAny

findAny 方法将返回当前流中的任意元素。它可以与其他流操作结合使用

举个例子:找到一道素菜。你可以结合使用 filter 和 findAny 方法来实现这个查询

    /**
     * 需求:  找到一道素菜  
     */
    public static Optional<Dish> randomVeDish(List<Dish> dishes) {
         return dishes.stream()
                 .filter(Dish::isVegetarian)
                 .findAny();
    }

流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。不过慢着代码里面的 Optional 是个什么玩意儿?

Optional<T> 类( java.util.Optional )是一个容器类,代表一个值存在或不存在。在上面的代码中, findAny 可能什么元素都没找到。Java 8的库设计人员引入了 Optional<T> ,这样就不用返回容易出问题的 null 了。

几个常用的方法

  • isPresent() 将在 Optional 包含值的时候返回 true , 否则返回 false 。
  • ifPresent(Consumer<T> block) 会在值存在的时候执行给定的代码块。它让你传递一个接收 T 类型参数,并返回 void 的Lambda表达式。
  • T get() 会在值存在时返回值,否则抛出一个 NoSuchElement 异常。
  • T orElse(T other) 会在值存在时返回值,否则返回一个默认值。

Java 8 -  Stream流骚操作解读


查找第一个元素 findFirst

有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List 或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个 findFirst方法,它的工作方式类似于 findany 。

例如,给定一个数字列表, 找出第一个平方能被3整除的数

    /**
     * 需求:     给定一个数字列表, 找出第一个平方能被3整除的数
     */
    public static Optional<Integer> xxxx() {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6);
        return  list.stream()
                .map(x -> x *x)
                .filter(i -> i % 3 == 0)
                .findFirst();
    }

何时使用 findFirst 和 findAny

什么会同时有 findFirst 和 findAny 呢?————–>并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个, 使用 findAny ,因为它在使用并行流时限制较少。

Java 8 -  Stream流骚操作解读

上一篇:21.2.3总结


下一篇:2020-2021 “Orz Panda” Cup Programming Contest E. Encryption of Orz Pandas(生成函数,FFT)