Java8新特性 | Stream流详解

函数式编程思想:

​ 面向对象思想需要关注用什么对象完成什么事情,儿函数式编程思想类似于我们数学中的函数。它关注的是对数据进行了什么操作。

优点:

  • 代码简介,开发快速
  • 接近自然语言,易于理解
  • 易于"并发编程"

Stream流前置知识

一、Lambda表达式

概述

Lambda是JDK8中的一个语法糖。他可以对某些匿名内部类的写法进行简化,他是函数式编程思想的一个重要体现。让我们不用关注是什么对象,而是更关注我们对数据进行了什么操作。

核心原则:可推倒可省略

不关注类名,方法名。只关注方法体!

基本格式

(参数类型 参数名称) ->{
    方法体;
}
复制代码

例一:多线程中创建线程的写法

new Thread(new Runnable(){
    @Override
    public void run(){
        System.out.println("我是新建线程1的输出");
    }
}).start();
复制代码

Lambda写法:

new Thread(()->{
    System.out.println("我是新建线程2的输出")
}).start();
复制代码

例二:

现有方法定义如下,其中IntBinaryOperation是一个接口,先试用匿名内部类的写法调用该方法,再用Lambda写法。

public class Lambda01 {
    public static void main(String[] args) {
        System.out.println(calculateNum(new IntBinaryOperator() {
            @Override
            public int applyAsInt(int left, int right) {
                return left + right;
            }
        }));

    }

    /**
     * 操作两个数
     * @param binaryOperator
     * @return applyAsInt
     */
    public static int calculateNum(IntBinaryOperator binaryOperator){
        int a = 10;
        int b = 20;
        return binaryOperator.applyAsInt(a,b);
    }

}

复制代码

Lambda写法:

public class Lambda01 {
    public static void main(String[] args) {
        System.out.println(calculateNum((int left,int right)->{
            return left+right;
        }));
    }
    
    /**
     * 操作两个数
     * @param binaryOperator
     * @return applyAsInt
     */
    public static int calculateNum(IntBinaryOperator binaryOperator){
        int a = 10;
        int b = 20;
        return binaryOperator.applyAsInt(a,b);
    }

}

复制代码

Lambda省略规则

  • 参数类型可以省略
  • 方法体中如果只有一句代码,{},return和唯一一句代码的;可以省略
  • 方法只有一个参数时()可以省略

Lambda总结

1.使用前提:

Lambda表达式语法是非常简洁的,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:

  • 方法的参数或局部变量类型必须是接口才能使用Lambda
  • 接口中有且仅有一个抽象方法(@FunctionalIneterface)

2.与匿名内部类的对比

  • 所需类型不同

    匿名内部类的类型可以是:类,抽象类,接口。

    Lambda表达式需要的类型必须是接口

  • 抽象发发数量不同

    匿名内部类所需的接口中的抽象方法的数量是随意的。

    Lambda表达式所需的接口中只能有一个抽象方法

  • 实现原理不同

    匿名内部类是在编译后形成.class

    Lambda表达式是在程序运行的时候动态生成.class

二、方法引用 | ::

在使用Lambda时,如果方法体中只有一个方法的调用的话(包括构造方法),我们可以用发发引用进一步简化代码

类名或者对象名 :: 方法名
复制代码

推荐用法:

如果lambda方法体中只有一行代码,并且是方法的调用。

java8方法引用有四种形式:

  • 静态方法引用       :   ClassName :: staticMethodName
  • 构造器引用        :   ClassName :: new
  • 类的任意对象的实例方法引用:   ClassName :: instanceMethodName
  • 特定对象的实例方法引用  :   object :: instanceMethodName

三、Optional

概述

本质上,Optional是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空。我们要知道,Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现。但是 Optional 的意义显然不止于此。我们知道,任何访问对象方法或属性的调用都可能导致 NullPointerException,在这里,我举个简单的例子来说明一下:

我们在编写代码的时候经常会出现空指针异常,为了避免空指针异常我们需要做各种非空的判断:

Author author = getAuthor();
if(author != null){
    System.out.println(author.getName());
}
复制代码

尤其是对象中的属性还是一个对象的情况下。这种判断会更多。而过多的判断会让我们的代码显得很臃肿。

Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

使用

Optional的构造函数

Optional 的三种构造方式:Optional.of(obj)Optional.ofNullable(obj) 和明确的 Optional.empty()

Optional.of(obj):它要求传入的 obj 不能是 null 值的, 否则直接报NullPointerException 异常。 Optional.ofNullable(obj):它以一种智能的,宽容的方式来构造一个 Optional 实例。来者不拒,传 null 进到就得到 Optional.empty(),非 null 就调用 Optional.of(obj). Optional.empty():返回一个空的 Optional 对象。

Optional的常用函数

isPresent:如果值存在返回true,否则返回false。 ifPresent:如果Optional实例有值则为其调用consumer,否则不做处理 get:如果Optional有值则将其返回,否则抛出NoSuchElementException。因此也不经常用。 orElse:如果有值则将其返回,否则返回指定的其它值。 orElseGet:orElseGet与orElse方法类似,区别在于得到的默认值。orElse方法将传入的字符串作为默认值,orElseGet方法可以接受Supplier接口的实现用来生成默认值 orElseThrow:如果有值则将其返回,否则抛出supplier接口创建的异常。 filter:如果有值并且满足断言条件返回包含该值的Optional,否则返回空Optional。 map:如果有值,则对其执行调用mapping函数得到返回值。如果返回值不为null,则创建包含mapping返回值的Optional作为map方法返回值,否则返回空Optional。 flatMap:如果有值,为其执行mapping函数返回Optional类型返回值,否则返回空Optional。

Optional 应该怎样用 在使用 Optional 的时候需要考虑一些事情,以决定什么时候怎样使用它。重要的一点是 Optional 不是 Serializable。因此,它不应该用作类的字段。如果你需要序列化的对象包含 Optional 值,Jackson 库支持把 Optional 当作普通对象。也就是说,Jackson 会把空对象看作 null,而有值的对象则把其值看作对应域的值。这个功能在 jackson-modules-java8 项目中。Optional 主要用作返回类型,在获取到这个类型的实例后,如果它有值,你可以取得这个值,否则可以进行一些替代行为。Optional 类可以将其与流或其它返回 Optional 的方法结合,以构建流畅的API。我们来看一个示例,我们不使用Optional写代码是这样的

public String getName(User user){
	if(user == null){
		return "Unknown";
	}else return user.name();
}
复制代码

接着我们来改造一下上面的代码,使用Optional来改造,我们先来举一个Optional滥用,没有达到流畅的链式API,反而复杂的例子,如下

public String getName(User user){
	Optional<User> u = Optional.ofNullable(user);
	if(!u.isPresent()){
		return "Unknown";
	}else return u.get().name();
}
复制代码

这样改写非但不简洁,而且其操作还是和第一段代码一样。无非就是用isPresent方法来替代原先user==null。这样的改写并不是Optional正确的用法,我们再来改写一次。

public String getName(User user){
	return Optional.ofNullable(user)
							.map(u -> u.name)
							.orElse("Unknown");
}

复制代码

这样才是正确使用Optional的姿势。那么按照这种思路,我们可以安心的进行链式调用,而不是一层层判断了。当然,我们还可以通过getter方式,对代码进行进一步缩减(前提是User要有getter方法哦),如下

String result = Optional.ofNullable(user)
	.flatMap(User::getAddress)
	.flatMap(Address::getCountry)
	.map(Country::getIsocode)
	.orElse("default");
复制代码

四、常用的函数式接口

JDK8 之前:

interface{
    静态常量;
    抽象方法;
}
复制代码

JDK8 之后:

interface{
    静态常量;
    抽象方法;
    默认方法;
    静态方法;
}
复制代码

默认方法通过实例调用,静态方法可以通过接口名调用;

默认方法可以被继承,实现类可以直接调用接口的默认方法,也可以重写接口默认方法;

静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用。

函数式接口

有且仅有一个抽象方法,就是函数式接口。在java.util.function包中。一般函数式接口中还包含默认方法,通常用语配合函数式接口使用。

@FunctionalIneterface

所修饰的接口只能定义一个抽象方法。--->函数式接口。

Supplier

有参无返回值的接口,用来生产数据的。

这是一个函数式接口,其函数式方法是get() 。
自:
1.8
类型形参:
<T> – 此供应商提供的结果类型
    
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

复制代码

对应的Lambda表达式:

public class SupplierTest {
    /**
     * 输出 max:66
     * @param args
     */
    public static void main(String[] args) {
        fun1(() ->{
            int arr[] = {22,33,11,44,55,66,12};
            //计算出数组中的最大值
            Arrays.sort(arr);
            return arr[arr.length-1];
        });
    }
    /**
     * 完成数据的处理,例如找最大值
     */
    private static void fun1(Supplier<Integer> supplier){
        Integer max = supplier.get();
        System.out.println("max"+max);
    }
}
复制代码

Consumer

有参无返回值的接口,用来消费数据,使用的时候需要指定一个泛型来定义参数类型

@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);

    //如果一个方法的参数和返回值全部是Consumer类型就可以实现连续操作:
    //消费数据之前 先做一个操作,再做一个操作,在消费: consumer1.andThen(consumer2).accept(参数);
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

复制代码

Lambda使用:

public class ConsumerTest {
    public static void main(String[] args) {
        test(msg->{
            //转小写
            msg = msg.toLowerCase();
            System.out.println(msg);
        });
    }

    public static void test(Consumer<String> consumer){
        consumer.accept("HELLO,WORLD!");
    }
}

复制代码

Function

有参有返回值的接口,根据一个类型的数据得到另一个类型的数据,前者成为前置条件,后者成为后置条件。

首先我们已经知道了Function是一个泛型类,其中定义了两个泛型参数T和R,在Function中,T代表输入参数,R代表返回的结果。

Function 就是一个函数,其作用类似于数学中函数的定义 ,(x,y)跟<T,R>的作用几乎一致。
										R=function(T)
所以Function中没有具体的操作,具体的操作需要我们去为它指定,因此apply具体返回的结果取决于传入的lambda表达式。
    
 R apply(T t);
复制代码
/**表示接受一个参数并产生结果的函数。
这是一个功能接口,其功能方法是apply(Object) 。
自:
1.8
类型形参:
<T> – 函数输入的类型
<R> - 函数结果的类型*/

@FunctionalInterface
public interface Function<T, R> {

/**返回一个组合函数,该函数首先将before函数应用于其输入,然后将此函数应用于结果。 如果对任一函数的评估引发异常,则将其转发给组合函数的调用者。
形参:
before – 在应用此函数之前要应用的函数
类型形参:
<V> – before函数和组合函数的输入类型
返回值:
首先应用before函数然后应用此函数的组合函数
*/

        R apply(T t);            
            
    }
复制代码

lambda用法:

public class FunctionTest {
    public static void main(String[] args) {
        // 6
            fun(arr->{
               return arr+1;
            });
    }

    //数组转字符串
    public static void fun(Function<Integer, Integer> fun1){
        System.out.println(fun1.apply(5));
    }
}

复制代码

Predicate

Predicate是个断言式接口其参数是<T,boolean>,也就是给一个参数T,返回boolean类型的结果。跟Function一样,Predicate的具体实现也是根据传入的lambda表达式来决定的。

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
}
复制代码

基本使用:

import java.util.function.Predicate;

//字符串很长吗:true
public class Demo01Predicate {
    public static void main(String[] args) {
        method(s -> s.length() > 5);
    }

    private static void method(Predicate<String> predicate) {
        boolean veryLong = predicate.test("HelloWorld");
        System.out.println("字符串很长吗:" + veryLong);
    }
}

复制代码

Stream流

为什么学习Stream流?

  • 工作需要
  • 大数量下处理集合效率更高
  • 代码可读性高
  • 消灭嵌套地狱

概述

Java8的Stream使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式操作。(类似于工厂中传送带)。可以更方便的让我们对集合或数组操作。

请添加图片描述

Stream产生的背景:

随着项目复杂度越来越高,数据源越来越多,有些数据无法通过SQL语句来查询,需要后期的组合筛选等,这就需要更高效的操作手段,如果使用遍历不但写法复杂,程序也会变得很复杂,Stream的出现大大增加了工作效率和程序的美观和效率。

2.使用案例

package stream.apple;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @ClassName: Stream01
 * @Description:
 * @author: 结构化思维wz
 * @date: 2021/12/7 21:34
 */


public class Stream01 {
    private static List<Apple> appleStore = new ArrayList<>();
    static {
        appleStore.add(new Apple(1,Color.RED,400,"重庆"));
        appleStore.add(new Apple(2,Color.GREEN,300,"河北"));
        appleStore.add(new Apple(3,Color.RED,500,"湖南"));
        appleStore.add(new Apple(4,Color.YELLOW,400,"天津"));
        appleStore.add(new Apple(5,Color.RED,500,"北京"));
    }

    /**
     * 统计指定颜色的苹果
     */
    public static void main(String[] args) {
        List<Apple> redApple = new ArrayList<>();
        for (Apple apple : appleStore){
            if (apple.getColor().equals(Color.RED)){
                redApple.add(apple);
            }
        }
        redApple.forEach(System.out::println);

        List<Apple> list = appleStore.stream()
                .filter(apple -> apple.getColor().equals(Color.RED) && apple.getWeight()>400)
                .collect(Collectors.toList());
        list.forEach(System.out::println);
    }
    
}

//控制台输出
Apple(id=1, color=RED, weight=400, origin=重庆)
Apple(id=3, color=RED, weight=500, origin=湖南)
Apple(id=5, color=RED, weight=500, origin=北京)
Apple(id=3, color=RED, weight=500, origin=湖南)
Apple(id=5, color=RED, weight=500, origin=北京)

复制代码

通过上面的例子:可以看见流的方便之处,不仅仅是代码简洁,如果过滤的信息更多if就会更多,代码很不美观。使用Stream流只需要:filter(apple ->apple.getColor.equals("RED")&&apple.getWeight()>300)。还可以进行 统计,分组等等...操作!

Stream流的两个基础特征:

  • Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格。这样做可以对操作进行优化,比如延迟执行和短路。
  • 内部迭代:以前集合遍历都是通过显示的在集合外部进行迭代,Stream提供了内部迭代的方式,流可以直接调用遍历方法。

当时用一个流的时候,通常包括三个基本步骤:

  1. 获取一个数据源
  2. 数据转换
  3. 执行操作获取想要的结果

每次转换原有的Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

请添加图片描述

Stream流的创建

顺序流和并行流:

请添加图片描述

java.util.stream.Stream<T>是Java8新加入的最常用的流接口。

获取一个流的方式非常简单,有以下几种方式:

  1. 所有的Collection集合都可以通过Stream默认的方法获取流。(注意Map如何创建流)

    List<String> list = Arrays.asList("a","b","c");
    //创建一个顺序流
    Stream<String> stream = list.stream();
    //创建一个并行流
    Stream<String> parallelStream = list.parallelStream();
    复制代码

    Map如何创建Stream:Map有key,value还有表示key,value整体的Entry。

    //创建一个Map:
    Map<String, String> someMap = new HashMap<>();
    //获取Map的entrySet:
    Set<Map.Entry<String, String>> entries = someMap.entrySet();
    //获取map的key:
    Set<String> keySet = someMap.keySet();
    //获取map的value:
    Collection<String> values = someMap.values();
    
    //除了Map没有stream,其他两个都有stream方法:
    Stream<Map.Entry<String, String>> entriesStream = entries.stream();
            Stream<String> valuesStream = values.stream();
            Stream<String> keysStream = keySet.stream();
    复制代码
  2. java.util.Arrays.stream(T[] array)通过数组创建流。

    int[] array = {1,3,4,5,6,7};
    //通过数组创建一个流
    IntStream stream = Arrays.stream(array);
    复制代码
  3. 使用Stream的静态方法。

    //创建Stream最简单的方式是直接用Stream.of()静态方法,传入可变参数即创建了一个能输出确定元素的Stream:
    Stream<String> stream = Stream.of("A", "B", "C", "D");   
    // forEach()方法相当于内部循环调用,
    // 可传入符合Consumer接口的void accept(T t)的方法引用:
    stream.forEach(System.out::println);
    复制代码

    这种方式基本上没有实质性的用途。

  4. 基于Supplier

    创建Stream还可以通过Stream.generate()方法,它需要传入一个Supplier对象:

    Stream<String> s = Stream.generate(Supplier<String> sp);
    复制代码

    基于Supplier创建的Stream会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。

    例如,我们编写一个能不断生成自然数的Supplier,它的代码非常简单,每次调用get()方法,就生成下一个自然数:

    public class StreamDemo {
        public static void main(String[] args) {
            Stream<Integer> nature = Stream.generate(new NatualSupplier());
            //注意要给定一个范围再输出,不然会死循环
            nature.limit(10).forEach(System.out::println);
        }
    }
    
    class NatualSupplier implements Supplier<Integer> {
        int n = 0;
        @Override
        public Integer get() {
            n++;
            return n;
        }
    }
    复制代码

    上述代码我们用一个Supplier<Integer>模拟了一个无限序列(当然受int范围限制不是真的无限大)。如果用List表示,即便在int范围内,也会占用巨大的内存,而Stream几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。

  5. 其他方法

    创建Stream的第三种方法是通过一些API提供的接口,直接获得Stream

    例如,Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:此方法对于按行遍历文本文件十分有用。

    try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
        ...
    }
    复制代码

    另外,正则表达式的Pattern对象有一个splitAsStream()方法,可以直接把一个长字符串分割成Stream序列而不是数组:

    Pattern p = Pattern.compile("\\s+");
    Stream<String> s = p.splitAsStream("The quick brown fox jumps over the lazy dog");
    s.forEach(System.out::println);
    复制代码
  6. 基本类型

    因为Java的范型不支持基本类型,所以我们无法用Stream<int>这样的类型,会发生编译错误。为了保存int,只能使用Stream<Integer>,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStreamLongStreamDoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率。

使用Stream流

请添加图片描述

流模型的操作很丰富,这里介绍一些常用的API:

  • 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。
  • 终结方法:返回值不再是Stream接口自身的方法,因此不支持链式调用。例如 countforEach方法。

先总结一下Stream提供的常用操作有:

(延迟)转换操作:map()filter()sorted()distinct()

(延迟)合并操作:concat()flatMap()

(终结)聚合操作:reduce()collect()count()max()min()sum()average()

(终结)其他操作:allMatch(), anyMatch(), forEach()

常用转换操作

map()

map()方法用于将一个Stream的每个元素映射成另一个元素并转换成一个新的Stream;通过若干步map转换,可以写出逻辑简单、清晰的代码。可以将一种元素类型转换成另一种元素类型。

Stream.map()Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream

请添加图片描述

所谓map操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如:对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:

            f(x) = x * x

                  │
                  │
  ┌───┬───┬───┬───┼───┬───┬───┬───┐
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   2   3   4   5   6   7   8   9 ]

  │   │   │   │   │   │   │   │   │
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   4   9  16  25  36  49  64  81 ]
复制代码

map()方法接收的对象是Function接口对象,利用map(),不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:

public class Main {
    public static void main(String[] args) {
        //Arrays.asList返回可变的list,而List.of返回的是不可变的list(Java9新语法)
        List.of("  Apple ", " pear ", " ORANGE", " BaNaNa ")
                .stream()
                .map(String::trim) // 去空格
                .map(String::toLowerCase) // 变小写
                .forEach(System.out::println); // 打印
    }
}
复制代码

filter()

使用filter()方法可以对一个Stream的每个元素进行测试,通过测试的元素被过滤后生成一个新的Stream

Stream.filter()Stream的另一个常用转换方法。

所谓filter()操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream

请添加图片描述

例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:

            f(x) = x % 2 != 0

                  │
                  │
  ┌───┬───┬───┬───┼───┬───┬───┬───┐
  │   │   │   │   │   │   │   │   │
  ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼   ▼

[ 1   2   3   4   5   6   7   8   9 ]

  │   X   │   X   │   X   │   X   │
  │       │       │       │       │
  ▼       ▼       ▼       ▼       ▼

[ 1       3       5       7       9 ]
复制代码
   public static void main(String[] args) {
        IntStream.of(1,2,3,4,5,6,7,8,9).filter(x -> x%2!=0).forEach(System.out::println);
    }
复制代码

filter()方法接收的对象是Predicate接口对象,filter()除了常用于数值外,也可应用于任何Java对象。例如从苹果中过滤红苹果:

public class Stream2 {
    private static List<Apple> appleStore = new ArrayList<>();
    static {
        appleStore.add(new Apple(1,Color.RED,400,"重庆"));
        appleStore.add(new Apple(2,Color.GREEN,300,"河北"));
        appleStore.add(new Apple(3,Color.RED,500,"湖南"));
        appleStore.add(new Apple(4,Color.YELLOW,400,"天津"));
        appleStore.add(new Apple(5,Color.RED,500,"北京"));
    }

    public static void main(String[] args) {
        appleStore.stream().filter(apple -> apple.getColor().equals(Color.RED)).forEach(System.out::println);
    }
}

复制代码

排序sorted()

Stream的元素进行排序十分简单,只需调用sorted()方法:

public class Main {
    public static void main(String[] args) {
        List<String> list = List.of("Orange", "apple", "Banana")
            .stream()
            .sorted()
            .collect(Collectors.toList());
        System.out.println(list);
    }
}

//[Banana, Orange, apple]
复制代码

此方法要求Stream的每个元素必须实现Comparable接口。如果要自定义排序,传入指定的Comparator即可:

List<String> list = List.of("Orange", "apple", "Banana")
    .stream()
    .sorted(String::compareToIgnoreCase)
    .collect(Collectors.toList());
复制代码

注意sorted()只是一个转换操作,它会返回一个新的Stream

去重distinct()

对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct()

List.of("A", "B", "A", "C", "B", "D")
    .stream()
    .distinct()
    .collect(Collectors.toList()); // [A, B, C, D]
复制代码

截取

截取操作常用于把一个无限的Stream转换成有限的Streamskip()用于跳过当前Stream的前N个元素,limit()用于截取当前Stream最多前N个元素:

List.of("A", "B", "C", "D", "E", "F")
    .stream()
    .skip(2) // 跳过A, B
    .limit(3) // 截取C, D, E
    .collect(Collectors.toList()); // [C, D, E]
复制代码

截取操作也是一个转换操作,将返回新的Stream

合并concat()

将两个Stream合并为一个Stream可以使用Stream的静态方法concat()

Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
复制代码

flatMap

如果Stream的元素是集合:

Stream<List<Integer>> s = Stream.of(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5, 6),
        Arrays.asList(7, 8, 9));
复制代码

而我们希望把上述Stream转换为Stream<Integer>,就可以使用flatMap()

Stream<Integer> i = s.flatMap(list -> list.stream());
复制代码

因此,所谓flatMap(),是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream

┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
                     │
                     │flatMap(List -> Stream)
                     │
                     │
                     ▼
   ┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
   │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
   └───┴───┴───┴───┴───┴───┴───┴───┴───┘
复制代码

常用聚合操作

reduce()

reduce()方法将一个Stream的每个元素依次作用于BinaryOperator,并将结果合并。reduce()是聚合方法,聚合方法会立刻对Stream进行计算。

Stream.reduce()则是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果。

public static void main(String[] args) {
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (acc, n) -> acc + n);
        System.out.println(sum); // 45
    }
复制代码

reduce()方法传入的对象是BinaryOperator接口,reduce()操作首先初始化结果为指定值(这里是0),紧接着,reduce()对每个元素依次调用(acc, n) -> acc + n,其中,acc是上次计算的结果。

如果去掉初始值,我们会得到一个Optional<Integer>

Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
    System.out.println(opt.get());
}
复制代码

这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。

利用reduce(),我们还可以把求和改为求乘积:

 public static void main(String[] args) {
     //注意:计算求积时,初始值必须设置为1。
        int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
        System.out.println(s); // 362880
    }
复制代码

**灵活的运用reduce()也可以对Java对象进行操作。**下面的代码演示了如何将配置文件的每一行配置通过map()reduce()操作聚合成一个Map<String, String>

public class Main {
    public static void main(String[] args) {
        // 按行读取配置文件:
        List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> map = props.stream()
                // 把k=v转换为Map[k]=v:
                .map(kv -> {
                    String[] ss = kv.split("\\=", 2);
                    return Map.of(ss[0], ss[1]);
                })
                // 把所有Map聚合到一个Map:
                .reduce(new HashMap<String, String>(), (m, kv) -> {
                    m.putAll(kv);
                    return m;
                });
        // 打印结果:
        map.forEach((k, v) -> {
            System.out.println(k + " = " + v);
        });
    }
}
复制代码

其他聚合方法

:one:除了reduce()collect()外,Stream还有一些常用的聚合方法:

  • count():用于返回元素个数;
  • max(Comparator<? super T> cp):找出最大元素;
  • min(Comparator<? super T> cp):找出最小元素。

:two:针对IntStreamLongStreamDoubleStream,还额外提供了以下聚合方法:

  • sum():对所有元素求和;
  • average():对所有元素求平均数。

:three:还有一些方法,用来测试Stream的元素是否满足以下条件:

  • boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件;
  • boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件。

forEach()

最后一个常用的方法是forEach(),它可以循环处理Stream的每个元素,我们经常传入System.out::println来打印Stream的元素:

Stream<String> s = ...
s.forEach(str -> {
    System.out.println("Hello, " + str);
});
复制代码

输出集合collect()

Stream可以输出为集合:

Stream通过collect()方法可以方便地输出为ListSetMap,还可以分组输出。

因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toListtoSettoMap比较常用,另外还有toCollectiontoConcurrentMap等复杂一些的用法。

输出为List

reduce()只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。

下面的代码演示了如何将一组String先过滤掉空字符串,然后把非空字符串保存到List中:

import java.util.*;
import java.util.stream.*;
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("Apple", "", null, "Pear", "  ", "Orange");
        List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
        System.out.println(list);
    }
}
复制代码

Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()对象,它实际上是一个Collector实例,通过类似reduce()的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。

类似的,collect(Collectors.toSet())可以把Stream的每个元素收集到Set中。

输出为数组

把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()方法,并传入数组的“构造方法”:

List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
复制代码

注意到传入的“构造方法”是String[]::new,它的签名实际上是IntFunction<String[]>定义的String[] apply(int),即传入int参数,获得String[]数组的返回值。

输出为Map

如果我们要把Stream的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map时需要key和value,因此,我们要指定两个映射函数,分别把元素映射为key和value:

import java.util.*;
import java.util.stream.*;
public class Main {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
        Map<String, String> map = stream
                .collect(Collectors.toMap(
                        // 把元素s映射为key:
                        s -> s.substring(0, s.indexOf(':')),
                        // 把元素s映射为value:
                        s -> s.substring(s.indexOf(':') + 1)));
        System.out.println(map);
    }
}

复制代码

分组输出groupingBy()

请添加图片描述

Stream还有一个强大的分组功能,可以按组输出。我们看下面的例子:

import java.util.*;
import java.util.stream.*;
 public static void main(String[] args) {
     //substring() 方法返回字符串的子字符串。
        List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
        Map<String, List<String>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
        System.out.println(groups);
    }
}

复制代码

分组输出使用Collectors.groupingBy(),它需要提供两个函数:一个是分组的key,这里使用s -> s.substring(0, 1),表示只要首字母相同的String分到一组,第二个是分组的value,这里直接使用Collectors.toList(),表示输出为List,上述代码运行结果如下:

{
    A=[Apple, Avocado, Apricots],
    B=[Banana, Blackberry],
    C=[Coconut, Cherry]
}
复制代码

可见,结果一共有3组,按"A""B""C"分组,每一组都是一个List

假设有这样一个Student类,包含学生姓名、班级和成绩:

class Student {
    int gradeId; // 年级
    int classId; // 班级
    String name; // 名字
    int score; // 分数
}
复制代码

如果我们有一个Stream<Student>,利用分组输出,可以非常简单地按年级或班级把Student归类。

 Map<Integer, List<Student>> groups = list.stream()
                .collect(Collectors.groupingBy(s -> s.classId, Collectors.toList()));
        System.out.println(groups);
复制代码

文件按行读取

Files类的lines()方法可以把一个文件变成一个Stream,每个元素代表文件的一行内容:

try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
    ...
}
复制代码

此方法对于按行遍历文本文件十分有用。

本文参考:<廖雪峰老师的网站>

==如果看完觉得写的还不错,给个赞再走吧!主页还有更多肝文!==

猜你喜欢

转载自juejin.im/post/7096734559609290789