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 ,因为它在使用并行流时限制较少。


相关文章
|
3月前
|
安全 Java API
告别繁琐编码,拥抱Java 8新特性:Stream API与Optional类助你高效编程,成就卓越开发者!
【8月更文挑战第29天】Java 8为开发者引入了多项新特性,其中Stream API和Optional类尤其值得关注。Stream API对集合操作进行了高级抽象,支持声明式的数据处理,避免了显式循环代码的编写;而Optional类则作为非空值的容器,有效减少了空指针异常的风险。通过几个实战示例,我们展示了如何利用Stream API进行过滤与转换操作,以及如何借助Optional类安全地处理可能为null的数据,从而使代码更加简洁和健壮。
109 0
|
20天前
|
Java API 数据处理
探索Java中的Lambda表达式与Stream API
【10月更文挑战第22天】 在Java编程中,Lambda表达式和Stream API是两个强大的功能,它们极大地简化了代码的编写和提高了开发效率。本文将深入探讨这两个概念的基本用法、优势以及在实际项目中的应用案例,帮助读者更好地理解和运用这些现代Java特性。
|
1月前
|
Java 流计算
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
37 1
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
|
1月前
|
Java Shell 流计算
Flink-02 Flink Java 3分钟上手 Stream SingleOutputStreamOpe ExecutionEnvironment DataSet FlatMapFunction
Flink-02 Flink Java 3分钟上手 Stream SingleOutputStreamOpe ExecutionEnvironment DataSet FlatMapFunction
23 1
Flink-02 Flink Java 3分钟上手 Stream SingleOutputStreamOpe ExecutionEnvironment DataSet FlatMapFunction
|
2月前
|
存储 Java API
Java——Stream流详解
Stream流是JDK 8引入的概念,用于高效处理集合或数组数据。其API支持声明式编程,操作分为中间操作和终端操作。中间操作包括过滤、映射、排序等,可链式调用;终端操作则完成数据处理,如遍历、收集等。Stream流简化了集合与数组的操作,提升了代码的简洁性
82 11
Java——Stream流详解
|
1月前
|
存储 Java 数据处理
Flink-01 介绍Flink Java 3分钟上手 HelloWorld 和 Stream ExecutionEnvironment DataSet FlatMapFunction
Flink-01 介绍Flink Java 3分钟上手 HelloWorld 和 Stream ExecutionEnvironment DataSet FlatMapFunction
35 1
|
2月前
|
Java API C++
Java 8 Stream Api 中的 peek 操作
本文介绍了Java中`Stream`的`peek`操作,该操作通过`Consumer&lt;T&gt;`函数消费流中的每个元素,但不改变元素类型。文章详细解释了`Consumer&lt;T&gt;`接口及其使用场景,并通过示例代码展示了`peek`操作的应用。此外,还对比了`peek`与`map`的区别,帮助读者更好地理解这两种操作的不同用途。作者为码农小胖哥,原文发布于稀土掘金。
115 9
Java 8 Stream Api 中的 peek 操作
|
2月前
|
Java C# Swift
Java Stream中peek和map不为人知的秘密
本文通过一个Java Stream中的示例,探讨了`peek`方法在流式处理中的应用及其潜在问题。首先介绍了`peek`的基本定义与使用,并通过代码展示了其如何在流中对每个元素进行操作而不返回结果。接着讨论了`peek`作为中间操作的懒执行特性,强调了如果没有终端操作则不会执行的问题。文章指出,在某些情况下使用`peek`可能比`map`更简洁,但也需注意其懒执行带来的影响。
139 2
Java Stream中peek和map不为人知的秘密
|
2月前
|
Java 大数据 API
Java 流(Stream)、文件(File)和IO的区别
Java中的流(Stream)、文件(File)和输入/输出(I/O)是处理数据的关键概念。`File`类用于基本文件操作,如创建、删除和检查文件;流则提供了数据读写的抽象机制,适用于文件、内存和网络等多种数据源;I/O涵盖更广泛的输入输出操作,包括文件I/O、网络通信等,并支持异常处理和缓冲等功能。实际开发中,这三者常结合使用,以实现高效的数据处理。例如,`File`用于管理文件路径,`Stream`用于读写数据,I/O则处理复杂的输入输出需求。
|
2月前
|
Java 程序员 API
Java 8新特性之Lambda表达式与Stream API的探索
【9月更文挑战第24天】本文将深入浅出地介绍Java 8中的重要新特性——Lambda表达式和Stream API,通过实例解析其语法、用法及背后的设计哲学。我们将一探究竟,看看这些新特性如何让Java代码变得更加简洁、易读且富有表现力,同时提升程序的性能和开发效率。