Java 8 - Stream流骚操作解读

简介: Java 8 - Stream流骚操作解读

分类

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

我们来看个例子

可以分为两大类操作

  • filter 、 map 和 limit 可以连成一条流水线
  • collect 触发流水线执行并关闭它

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


中间操作

诸如 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());
    }

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

输出

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 方法(对流来说就是终端操作)。

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

【中间操作】

【终端操作】

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

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


筛选和切片

如何选择流中的元素?

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

用谓词筛选 filter

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

需求: 筛选出所有素菜

/**
     * 需求:  输出所有的素菜
     * @param dishList
     * @return
     */
    public static List<Dish> getVegetarianByStream(List<Dish> dishList){
        return dishList.stream()
                .filter(Dish::isVegetarian)
                .collect(Collectors.toList());
    }


筛选各异的元素 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);
    }


截短流 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 的结果不会以任何顺序排列。


跳过元素 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());
    }


映射

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

对流中每一个元素应用函数 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

【再来看个例子 】

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

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

这听起来正好该用 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());
    }


流的扁平化 flatMap

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

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

怎么实现呢?

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

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 类型的。 我们真正想要的是用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());
    }

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

使用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());
        }

使用 flatMap 方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容 。 所有使用 flatMap(Arrays::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中 && 和 || 运算符短路在流中的版本


什么是短路求值

有些操作不需要处理整个流就能得到结果。例如,你需要对一个用 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 类( java.util.Optional )是一个容器类,代表一个值存在或不存在。在上面的代码中, findAny 可能什么元素都没找到。Java 8的库设计人员引入了 Optional ,这样就不用返回容易出问题的 null 了。

几个常用的方法

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


查找第一个元素 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 ,因为它在使用并行流时限制较少。


相关文章
|
8天前
|
安全 Java 大数据
|
8天前
|
Java Unix Windows
|
6天前
|
自然语言处理 Java API
Java 8的Stream API和Optional类:概念与实战应用
【5月更文挑战第17天】Java 8引入了许多重要的新特性,其中Stream API和Optional类是最引人注目的两个。这些特性不仅简化了集合操作,还提供了更好的方式来处理可能为空的情况,从而提高了代码的健壮性和可读性。
28 7
|
6天前
|
Java API
Java 8新特性之Lambda表达式与Stream API
【5月更文挑战第17天】本文将介绍Java 8中的两个重要特性:Lambda表达式和Stream API。Lambda表达式是一种新的编程语法,它允许我们将函数作为参数传递给其他方法,从而使代码更加简洁。Stream API是一种用于处理集合的新工具,它提供了一种高效且易于使用的方式来处理数据。通过结合使用这两个特性,我们可以编写出更加简洁、高效的Java代码。
16 0
|
8天前
|
安全 Java API
Java Stream API详解与使用
Java Stream API是Java 8引入的特性,提供函数式操作处理集合,支持链式操作和并行处理,提升代码可读性和性能。关键点包括:延迟执行的中间操作(如filter, map)和触发计算的终端操作(如collect, forEach)。示例展示了如何从Person列表过滤出年龄大于20的姓名并排序。使用Stream时注意避免中间操作的副作用,终端操作后Stream不能复用,以及并行操作的线程安全性。
18 1
|
2天前
|
分布式计算 Java 程序员
Java 8新特性之Lambda表达式与Stream API
【5月更文挑战第21天】本文主要介绍了Java 8中的两个重要新特性:Lambda表达式和Stream API。Lambda表达式是Java 8引入的一种新的编程方式,它允许我们将函数作为参数传递给其他方法。而Stream API则是一种新的数据处理方式,它允许我们以声明式的方式处理数据,使得代码更加简洁易读。
|
3天前
|
Java 程序员 API
Java 8 Lambda 表达式和Stream API:概念、优势和实战应用
【5月更文挑战第20天】在Java 8中,Lambda 表达式和Stream API是两个非常强大的特性,它们显著改变了Java程序员处理数据和编写代码的方式。本篇技术文章将深入探讨这些特性的概念、优点,并提供实战示例,帮助理解如何有效地利用这些工具来编写更简洁、更高效的代码。
21 6
|
5天前
|
安全 Java API
Java进阶-Java Stream API详解与使用
效、更易于维护的代码,同时享受到函数式编程带来的好处。
18 2
|
5天前
|
Java 大数据 API
利用Java Stream API实现高效数据处理
在大数据和云计算时代,数据处理效率成为了软件开发者必须面对的重要挑战。Java 8及以后版本引入的Stream API为开发者提供了一种声明式、函数式的数据处理方式,极大提升了数据处理的效率和可读性。本文将详细介绍Java Stream API的基本概念、使用方法和性能优势,并通过实际案例展示如何在实际项目中应用Stream API实现高效数据处理。
|
6天前
|
Java
Java8 Stream 用法合集
Java8 Stream 用法合集