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


相关文章
|
4月前
|
安全 Java API
告别繁琐编码,拥抱Java 8新特性:Stream API与Optional类助你高效编程,成就卓越开发者!
【8月更文挑战第29天】Java 8为开发者引入了多项新特性,其中Stream API和Optional类尤其值得关注。Stream API对集合操作进行了高级抽象,支持声明式的数据处理,避免了显式循环代码的编写;而Optional类则作为非空值的容器,有效减少了空指针异常的风险。通过几个实战示例,我们展示了如何利用Stream API进行过滤与转换操作,以及如何借助Optional类安全地处理可能为null的数据,从而使代码更加简洁和健壮。
129 0
|
23天前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
40 6
|
23天前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
14天前
|
Rust 安全 Java
Java Stream 使用指南
本文介绍了Java中Stream流的使用方法,包括如何创建Stream流、中间操作(如map、filter、sorted等)和终结操作(如collect、forEach等)。此外,还讲解了并行流的概念及其可能带来的线程安全问题,并给出了示例代码。
|
25天前
|
安全 Java API
Java中的Lambda表达式与Stream API的高效结合####
探索Java编程中Lambda表达式与Stream API如何携手并进,提升数据处理效率,实现代码简洁性与功能性的双重飞跃。 ####
26 0
|
1月前
|
Java API 数据处理
探索Java中的Lambda表达式与Stream API
【10月更文挑战第22天】 在Java编程中,Lambda表达式和Stream API是两个强大的功能,它们极大地简化了代码的编写和提高了开发效率。本文将深入探讨这两个概念的基本用法、优势以及在实际项目中的应用案例,帮助读者更好地理解和运用这些现代Java特性。
|
2月前
|
Java 流计算
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
46 1
Flink-03 Flink Java 3分钟上手 Stream 给 Flink-02 DataStreamSource Socket写一个测试的工具!
|
2月前
|
Java Shell 流计算
Flink-02 Flink Java 3分钟上手 Stream SingleOutputStreamOpe ExecutionEnvironment DataSet FlatMapFunction
Flink-02 Flink Java 3分钟上手 Stream SingleOutputStreamOpe ExecutionEnvironment DataSet FlatMapFunction
26 1
Flink-02 Flink Java 3分钟上手 Stream SingleOutputStreamOpe ExecutionEnvironment DataSet FlatMapFunction
|
3月前
|
存储 Java API
Java——Stream流详解
Stream流是JDK 8引入的概念,用于高效处理集合或数组数据。其API支持声明式编程,操作分为中间操作和终端操作。中间操作包括过滤、映射、排序等,可链式调用;终端操作则完成数据处理,如遍历、收集等。Stream流简化了集合与数组的操作,提升了代码的简洁性
188 11
|
3月前
|
Java API C++
Java 8 Stream Api 中的 peek 操作
本文介绍了Java中`Stream`的`peek`操作,该操作通过`Consumer&lt;T&gt;`函数消费流中的每个元素,但不改变元素类型。文章详细解释了`Consumer&lt;T&gt;`接口及其使用场景,并通过示例代码展示了`peek`操作的应用。此外,还对比了`peek`与`map`的区别,帮助读者更好地理解这两种操作的不同用途。作者为码农小胖哥,原文发布于稀土掘金。
141 9
Java 8 Stream Api 中的 peek 操作