Stream API使用教程详解

文章内容

JDK8的重要更新除去Lambda之外还有Stream,两者结合使用为操作和计算数据提供了极大的便利。

一、Stream简介

1、Stream是什么?

Stream就是【流】的意思,与 java.io包中的输入流,输出流是两个不同的概念

Stream流是JDK8新增用来处理集合、数组、文件等数据,借助Lambda表达式,极大提高编程效率和程序可读性,同时拥有串行和并行两种数据处理模式,并行模式可以充分利用多核CPU性能,通过 fork/join 方式拆解任务加速处理。

2、使用Stream的好处

  • 函数式编程:让Java原本臃肿的代码变的简洁,这当然是需要配合Lambda实现
  • 高效的并行处理机制,比之前的for循环加if…else,挨个元素处理速度要快上许多
  • 具有多种数据的处理实现,比如筛选,去重,转换,查询,遍历等内置操作

3、Stream的特点

  • 流与集合、数组、文件不同,不是数据结构,不存储数据,目的是处理数据,将处理结果返回或者转换
  • 流在计算数据时,如果需要使用到集合中元素,会取出使用,并不修改源数据,流只使用数据一次
  • 支持延迟计算,只有等到执行终止操作时才会执行计算,可以降低不必要的CPU资源浪费

4、Stream操作分类

  • 创建流:可以通过集合、数组、IO资源、Stream的构造函数创建
  • 中间操作:对数据的计算操作,比如筛选,去重,转换等操作,一个中间操作返回一个新的Stream,来支持连续计算
  • 终止操作:每个流只能有一次终止操作,终止之后流无法使用,会产生一个计算结果,可以根据需求转换为想要的结果类型

二、Stream运行流程

这里通过一个运费案例,通过 【代码实现】 +【 图解】解释清楚Stream计算数据时的流程!

1、需求

需求:获取运单价格大于 5000元 的运单编号

2、分析

  • 创建运单数据
  • 通过集合的stream方法创建流
  • 再通过调用流对象的 filter方法过滤出需要的数据【中间操作】
  • 再通过流对象的map方法获取想要的字段数据【中间操作】
  • 在通过collect方法将流对象转换为集合,终止流【终止操作】

3、代码实现

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WaybillMain {

    private static List<Waybill> waybills = new ArrayList<>();

    static {
        // 创建数据
        waybills.add(new Waybill(1L,"Y11111111111",new BigDecimal(1000),"钢材",new BigDecimal(200),"上海市"));
        waybills.add(new Waybill(2L,"Y22222222222",new BigDecimal(2000),"钢材",new BigDecimal(300),"郑州市"));
        waybills.add(new Waybill(3L,"Y33333333333",new BigDecimal(3000),"水泥",new BigDecimal(300),"北京市"));
        waybills.add(new Waybill(4L,"Y44444444444",new BigDecimal(4000),"水泥",new BigDecimal(400),"广州市"));
        waybills.add(new Waybill(5L,"Y55555555555",new BigDecimal(5000),"沙子",new BigDecimal(500),"上海市"));
        waybills.add(new Waybill(6L,"Y66666666666",new BigDecimal(6000),"板材",new BigDecimal(500),"深圳市"));
        waybills.add(new Waybill(7L,"Y77777777777",new BigDecimal(7000),"蔬菜",new BigDecimal(500),"杭州市"));
    }

    public static void main(String[] args) {
        // 1、获取运费大于5000的运单编号
        // 1) 通过集合的stream方法创建流
        Stream<Waybill> stream = waybills.stream();
        // 2) 通过 filter 方法筛选运单大于5000的运单
        Stream<Waybill> filterWaybill = stream.filter(item -> item.getPrice().compareTo(new BigDecimal(5000)) == 1);
        // 3) 获取筛选后的运单的编号
        Stream<String> wayNoStream = filterWaybill.map(Waybill::getWayNo);
        // 4) 将流转换为新的集合
        List<String> wayNoList = wayNoStream.collect(Collectors.toList());
        // 5) 遍历
        wayNoList.forEach(System.out::println);
    }
}

4、运行流程

Stream API使用教程详解插图

三、Stream创建

1、Stream流主要的创建方法

流可以用来处理数组、集合、IO资源等数据,而且分为【串行流】和【并行流】两种,它的创建方式主要分为以下几种:

  • 使用Collection下的stream() 方法【串行流】和parallelStream() 方法【并行流】
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
  • 使用Arrays中的stream() 方法,将数组转换为流
int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);
  • 使用Stream中的静态方法:of()、iterate()、generate()
    • 对于iterate和generate这种没有数据长度的流称为【无限流】,需要使用limit()来指定流长度
    • 比如generate是生成数据,生成多少数据?需要使用limit指定
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
// 参数1:为起始值
// 参数2:每次的值进行什么操作,再基于结果做下一次的运算
// limit:最多4次操作
List<Integer> iterate = Stream.iterate(1, x -> x * 3).limit(4).collect(Collectors.toList());
System.out.println(iterate);
// 生成 3 个随机数
Stream<Double> limit = Stream.generate(Math::random).limit(3);
limit.forEach(System.out::println);
  • 使用 BufferedReader.lines() 方法,将每行内容转成流
BufferedReader reader = new BufferedReader(new FileReader("D:stream.txt"));
Stream<String> lineStream = reader.lines();
lineStream.forEach(System.out::println);
  • 使用 Pattern.splitAsStream() 方法,将字符串分隔成流
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
stringStream.forEach(System.out::println);

2、串行流和并行流区别

1)stream()和parallelStream()创建流的区别

stream()方法创建的是【串行流】也可以叫【顺序流】,由主线程按顺序对流执行操作,而 parallelStream()方法创建的是【并行流】,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处:

Stream API使用教程详解插图2

如果流中的数据量足够大,并行流可以加快处速度。

2)通过parallel()把顺序流转换成并行流

除了直接创建并行流,还可以通过 parallel()把顺序流转换成并行流:

// 创建数组
Integer[] arr = {1,2,3,4,5,6,7,8,9,10};

// 通过 stream 转换为串行流,再通过 Stream 对象的 parallel 方法转换为并行流
Stream<Integer> integerStream = Arrays.stream(arr).parallel();
// 计算,并行流只能对无顺序要求的计算生效
// mao:对每一个数据 * 2
List<Integer> list = integerStream.map(x -> x * 2).collect(Collectors.toList());
list.forEach(System.out::println);

四、Stream操作分类

Stream API使用教程详解插图4
  • 无状态:元素的处理不受之前元素影响,比如:过滤,映射,转换类型
  • 有状态:该元素只有拿到所有元素之后才能继续下去,比如排序,去重
  • 非短路操作:必须处理完所有元素才能得到结果,比如:求最值,遍历
  • 短路操作:遇到某些符合条件的元素就可以得到最终结果,比如:获取第一个出现的数据

1、无状态操作【Stateless】

1)过滤-filter

作用:筛选出符合规则的元素

方法定义

接收 断言函数式接口 Predicate,接收一个参数,返回boolean类型结果
Stream<T> filter(Predicate<? super T> predicate);

案例:获取字符串数组中,字符串长度大于5的元素

// 定义数组
String[] strArr = {"hello","am","Stream","see you again","see you tomorrow"};
// 过滤
Stream<String> result = Arrays.stream(strArr).filter(str -> str.length() > 5);
// 遍历
result.forEach(System.out::println);

运行结果

Stream API使用教程详解插图6

2)映射-map、flatMap

a)map

map:对每一个元素进行指定操作后,返回新的元素,比如:数学运算,类型转换等操作

方法定义:

接收Function类型函数式接口,接收一个T类型参数,并返回一个R类型参数
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

案例:

  • 将数组中的字符串都转换为大写
  • 获取字符串长度
// 定义数组
String[] strArr = {"hello","am","Stream","see you again","see you tomorrow"};
// 转换为流对象
Stream<String> stream1 = Arrays.stream(strArr);
// 1、转换大写
Stream<String> upperCaseResult = stream1.map(str -> str.toUpperCase());
System.out.println("*********转换大写*********");
// 遍历
upperCaseResult.forEach(System.out::println);

// 2、获取每一个字符串长度
System.out.println("*********获取长度*********");
// 必须再次转换流,上一个流【stream1】执行了forEach的终止操作,已经关闭不能再使用
Stream<String> stream2 = Arrays.stream(strArr);
Stream<Integer> lengthResult = stream2.map(str -> str.length());
lengthResult.forEach(System.out::println);

运行结果:

Stream API使用教程详解插图8
b)flatMap

flatMap:可以将流中的每一个值转换成另一个流,然后把所有的流连接起来再变成一个流,这个方法也可以叫压平

方法定义:

接收 Function 类型函数式接口,与map方法定义类似,但是可以接受一个流对象
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

案例:给定单词列表[“See”,“You”],你想要返回列表[“S”,“e”,“e”, “Y”,“o”,“u”]

// 数组
String[] arr = {"See","You"};
// 转换流
Stream<String> stream = Arrays.stream(arr);
Stream<String> stringStream = stream.flatMap(str -> {
    /*
     将每一个单词按照 ""切割
     第一次:See---》["s","e","e"]
     第二次:You---》 ["Y","o","u"]
    */    String[] split = str.split("");
    // 将字符串数组转换成流,
    Stream<String> stream1 = Arrays.stream(split);
    return stream1;
});
stringStream.forEach(System.out::println);

运行结果:

Stream API使用教程详解插图10

3)转换

转换类型方法有:mapToInt、mapToLong、mapToDouble、flatMapToDouble、flatMapToInt、flatMapToLong

以上这些操作是map和flatMap的特例版,也就是针对特定的数据类型进行映射处理,返回的是指定的InteStream、LongStream、DoubleStream类型的流对象,包含一些数学运算

方法定义:

IntStream mapToInt(ToIntFunction<? super T> mapper);
 
LongStream mapToLong(ToLongFunction<? super T> mapper); 
 
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
 
IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
 
LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
 
DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

案例:计算数组的总字符长度

这里通过 mapToInt为例,其他的方法 同理

// 定义数组
String[] strArr = {"hello","am","Stream"};
// 转换为流对象,使用 mapToInt 方法获取每一个字符串长度,返回的是IntStream类型的流
IntStream intStream = Arrays.stream(strArr).mapToInt(str -> str.length());
// 调用sum方法计算和
int sum =  intStream.sum();
System.out.println(sum);

运行结果:

Stream API使用教程详解插图12

4)无序化-unordered

作用:unordered()操作不会执行任何操作来保障有序。它的作用是消除了流必须保持有序的约束,从而允许后续的操作,不必考虑排序,然后某些操作可以做一些特殊优化,注意:是“不保证有序”,不是“保证无序“

案例:

// 串行流
Stream.of(5, 2, 7, 3, 9, 1).unordered().forEach(System.out::println);
// 并行流
Stream.of(5, 2, 7, 3, 9, 1).parallel().unordered().forEach(System.out::println);

注意:

我们单单使用输出其实并不能看出什么端倪,在【JDK8】官方文档中有一句话对无序化做出了解释

  • 对于顺序流,顺序的存在与否不会影响性能,只影响确定性。如果流是顺序的,则在相同的源上重复执行相同的流管道将产生相同的结果;
  • 如果是非顺序流,重复执行可能会产生不同的结果。 对于并行流,放宽排序约束有时可以实现更高效的执行。
  • 在流有序时, 但用户不特别关心该顺序的情况下,使用 unordered 明确地对流进行去除有序约束可以改善某些有状态或终端操作的并行性能。

2、有状态操作【Stateful】

1)去重-distinct

作用:根据对象的hashCode()方法和equals()方法来确定是否是相同元素,进行去重

方法定义:

方法没有参数,返回一个去重后的流对象
Stream<T> distinct();

案例:

  • 基于【flatMap】案例,获取去重后的流,比如See You,其中e是重复的,我们对其去重
  • 对”helloworld”字符串去重【经典面试题,看看使用JDK8特性是多么简单】
// 1、定义数组
String[] arr = {"See","You"};
// 获取流,压平数据并去重
Stream<String> distinct = Arrays.stream(arr).flatMap(str -> Arrays.stream(str.split(""))).distinct();
distinct.forEach(System.out::println);

// 2、字符串去重
Stream<String> stream = Stream.of("helloworld").flatMap(str -> Arrays.stream(str.split(""))).distinct();
stream.forEach(System.out::println);

案例1运行结果:

Stream API使用教程详解插图14

案例2运行结果:

Stream API使用教程详解插图16

2)排序-sorted

作用:对流进行排序,该方法有两个重载,一个无参,一个接收 Comparator比较器,传入比较器可以自定义排序

方法定义:

Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);

案例:对数组进行排序

String[] strArr = {"hello","am","Stream","see you again","see you tomorrow"};
// 1、默认排序,按照ASCII码表排序,大写S=83,小写a=97
Stream<String> sorted1 = Arrays.stream(strArr).sorted();
System.out.println("******默认排序******");
sorted1.forEach(System.out::println);
// 2、按照字符串长度排序【自定义排序】
Stream<String> sorted2 = Arrays.stream(strArr).sorted((o1, o2) -> o1.length() - o2.length());
System.out.println("******按照字符串长度,自定义排序******");
sorted2.forEach(System.out::println);

运行结果:

Stream API使用教程详解插图18

3)指定数量-limit

作用:获取流中n个元素,并返回一个流对象

方法定义:

接收一个long类型的数量,返回Stream
Stream<T> limit(long maxSize);

案例:获取四个字符串

Stream.of("hello", "am", "Stream", "see you again", "see you tomorrow").limit(4).forEach(System.out::println);

运行结果:

Stream API使用教程详解插图20

4)跳过-skip

作用:跳过n个元素,获取其后的所有元素,也可以理解为偏移量

方法定义:

与limit类似,区别是功能不同
Stream<T> skip(long n);

案例:跳过前两个字符串

Stream.of("hello", "am", "Stream", "see you again", "see you tomorrow").skip(2).forEach(System.out::println);

运行结果:

Stream API使用教程详解插图22

5)peek

作用:不调整元素顺序和数量的情况下消费每一个元素,然后产生新的流,按文档上的说明,主要是用于对流执行的中间过程做debug的时候使用,因为Stream使用的时候一般都是链式调用的,所以可能会执行多次流操作,如果想看每个元素在多次流操作中间的流转情况,就可以使用这个方法实现

方法定义:

接收一个 消费型Consumer函数接口,返回Stream对象
Stream<T> peek(Consumer<? super T> action);

案例:每次对流计算之后,看当前的计算结果

List<String> collect = Stream.of("hello", "am", "Stream", "see you again", "see you tomorrow")
                .filter(str -> str.length() > 5)
                .peek(System.out::println)
                .map(str -> str.toUpperCase())
                .peek(System.out::println)
                .sorted(((o1, o2) -> o1.length() - o2.length()))
                .collect(Collectors.toList());
System.out.println("***********遍历集合******************");
collect.forEach(System.out::println);

运行结果:

Stream API使用教程详解插图24

至此【中间操作】介绍完毕,中间操作分为有状态和无状态,这写方法都可以配合连续使用

3、非短路操作【Unshort-circuiting】

1)遍历-forEach

作用:跟普通的for循环类似,不过这个可以支持多线程遍历,但是不保证遍历的顺序

方法定义:

接收一个 消费型Consumer函数接口,没有返回值,所以就不能继续往后操作了,直接终止流
void forEach(Consumer<? super T> action);

案例:遍历数组

String[] arr = {"hello", "am", "Stream", "see you again", "see you tomorrow"};
// 1、Lambda遍历
Arrays.stream(arr).forEach(str -> System.out.println(str));
// 2、方法引用遍历
Arrays.stream(arr).forEach(System.out::println);
// 3、对线程遍历
Arrays.stream(arr).parallel().forEach(System.out::println);
// 4、条件遍历,不推荐,建议使用 filter 过滤数据,这种写法看着代码很乱
Arrays.stream(arr).forEach(str -> {
    if(str.equalsIgnoreCase("am")) {
        System.out.println(str);
    }
});

2)转换数组-toArray

作用:将流转换为数组

方法定义:

  • 无参方法:转换为Object类型数组
  • 有参方法:转换成指定类型的数组
Object [] toArray();
<A> A[] toArray(IntFunction<A[]> generator);

案例:对数组去重,将去重后的数据放到新数组中

String[] arr = {"hello", "am", "Stream", "hello","Stream", "see you again", "see you tomorrow"};
// 去重
Stream<String> distinct = Arrays.stream(arr).distinct();
// 1、转换为Object类型数组
Object[] array = distinct.toArray();
System.out.println("********Object数组********");
System.out.println(Arrays.toString(array));

// 2、转换为Strinbg类型数组
String[] stringArr = Arrays.stream(arr).distinct().toArray(String[]::new);
System.out.println("********String数组********");
System.out.println(Arrays.toString(stringArr));

运行结果:

Stream API使用教程详解插图26

3)累加器-reduce

作用:有三个重载方法,作用是对流内元素做累进操作

方法定义:

  • 一个参数:对数据进行计算,返回一个Optional对象,接收BinaryOperator函数接口,BinaryOperator接口的抽象方法传入两个参数,并返回一个结果
  • 两个参数:对数据进行计算,参数1为初始计算值,也就是参数1先和数组中数据进行一次计算,参数2是一个BinaryOperator接口,返回的是运算结果
  • 三个参数:在第二个参数的基础上,追加了一个组合器参数,应用于并行计算,可以改变返回值类型
    • 组合器只在并行流中起作用,否则不执行组合器的方法
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);
a)案例1:分别使用一个参数和两个参数的方法进行累加计算
Integer[] arr = {1,2,3,4,5,6,7,8,9,10};
// 1、一个参数的reduce方法
// 因为reduce方法接受的是 BinaryOperator接口,该借口的抽象方法需要两个参数,所以这里写了x,y两个,分别将数组中的值传进去做累加计算
Optional<Integer> reduce = Arrays.stream(arr).reduce((x, y) -> x + y);
// 通过get方法获取Optional数据
Integer integer = reduce.get();
System.out.println("************一个参数************");
System.out.println(integer);

// 2、同样每个数据累加,但是也会累加上第一个参数,在本案例中也就是结果多+1
Integer reduce1 = Arrays.stream(arr).reduce(1, (x, y) -> x + y);
System.out.println("************两个参数************");
System.out.println(reduce1);

运行结果:

Stream API使用教程详解插图28
b)案例2:三个参数
Ⅰ)单线程计算
Integer[] arr = {1,2,3,4,5};
Integer reduce = Arrays.stream(arr).reduce(0, (x, y) -> {
    System.out.println(Thread.currentThread().getName() + "-x:" + x);
    System.out.println(Thread.currentThread().getName() + "-y:" + y);
    return x + y;
}, (a, b) -> {
    System.out.println(Thread.currentThread().getName() + "-a:" + a);
    System.out.println(Thread.currentThread().getName() + "-b:" + b);
    return a + b;
});
System.out.println("结果:" + reduce);

结果:

Stream API使用教程详解插图30

都是main线程在执行,并且没有执行a 和 b

Ⅱ)多线程运算

使用 parallel() 方法转换为并行流

Integer[] arr = {1,2,3,4,5};
// 添加上 parallel() 方法转换为并行流即可
Integer reduce = Arrays.stream(arr).parallel().reduce(0, (x, y) -> {
    System.out.println(Thread.currentThread().getName() + "-x:" + x);
    System.out.println(Thread.currentThread().getName() + "-y:" + y);
    return x + y;
}, (a, b) -> {
    System.out.println(Thread.currentThread().getName() + "-a:" + a);
    System.out.println(Thread.currentThread().getName() + "-b:" + b);
    return a + b;
});
System.out.println("结果:" + reduce);

结果:

Stream API使用教程详解插图32

可以看出创建了11条线程参与运算,转换为并行流后第三个参数方法才会执行,组合器的作用,其实是对参数2中的各个线程,产生的结果进行了再一遍的归约操作!

4)收集器-collect

作用:是一个终止操作,将流中的元素转换为各种类型的结果

方法定义:

  • 方法1:比较常用,将数据转换为指定的容器中,或者做求和、分组、分区、平均值等操作
  • 方法2:可以用来实现filter、map操作
<R, A> R collect(Collector<? super T, A, R> collector);

<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);
a)案例1:演示方法1的使用

Employee类准备

public class Employee {

    private Long id;

    private Integer age;

    private String name;

    private Double salary;

    public Employee(Long id, Integer age, String name, Double salary) {
        this.id = id;
        this.age = age;
        this.name = name;
        this.salary = salary;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + ''' +
                ", salary=" + salary +
                '}';
    }
}

收集器演示:

使用数组对象集合:一共演示 11 个方法,结果太长,可通过代码自行打印

public class StreamMain {

    private static List<Employee> employees = new ArrayList<>();

    static {
        // 创建数据
        employees.add(new Employee(1L,24,"苏小坡",5000.00));
        employees.add(new Employee(2L,24,"苏小由",6000.00));
        employees.add(new Employee(3L,25,"石小敢",6000.00));
        employees.add(new Employee(4L,23,"张小毫",6800.00));
        employees.add(new Employee(5L,23,"李小杰",6800.00));
        employees.add(new Employee(6L,31,"杜小甫",8000.00));
        employees.add(new Employee(7L,29,"辛小疾",8500.00));
    }

    public static void main(String[] args) {
        String[] arr = {"hello", "am", "Stream", "hello","Stream", "see you again", "see you tomorrow"};
        // 1、Collectors.toList(): 将数据转换为List集合
        List<String> list = Arrays.stream(arr).collect(Collectors.toList());

        // 2、Collectors.toSet(): 将数据转换为Set集合
        Set<String> set = Arrays.stream(arr).collect(Collectors.toSet());

        // 3、Collectors.toCollection(LinkedList::new): 将数据存储进指定的集合中
        LinkedList<String> linkedList = Arrays.stream(arr).collect(Collectors.toCollection(LinkedList::new));

        // 4、Collectors.counting(): 获取元素个数
        Long count = Arrays.stream(arr).collect(Collectors.counting());

        // 5、Collectors.summingInt():求和,方法接收一个 ToIntFunction 函数接口,接收一个值,返回一个值
        // 还有:summingDouble,summingLong方法,计算Double和Long类型的和

        Integer sumAge = employees.stream().collect(Collectors.summingInt(Employee::getAge));
        Double sumSalary = employees.stream().collect(Collectors.summingDouble(Employee::getSalary));
        Long sumId = employees.stream().collect(Collectors.summingLong(Employee::getId));

        // 6、Collectors.averagingInt():求平均值
        Double avgAge = employees.stream().collect(Collectors.averagingInt(Employee::getAge));

        // 7、Collectors.joining():根据指定字符拼接
        String joinName =  employees.stream().map(Employee::getName).collect(Collectors.joining(","));

        // 8、获取最大薪资【不建议使用】,推荐使用max方法
        Optional<Employee> maxSalary = employees.stream().collect(Collectors.maxBy((o1, o2) -> o1.getSalary().compareTo(o2.getSalary())));
        System.out.println(maxSalary.get());

        // 8、获取最小薪资【不建议使用】,推荐使用min方法
        Optional<Employee> minSalary = employees.stream().collect(Collectors.minBy((o1, o2) -> o1.getSalary().compareTo(o2.getSalary())));
        System.out.println(minSalary.get());

        // 9、Collectors.reducing():规约操作,这个方法也有三个重载,和reduce方法一样,此处对数据做指定运算,并指定一个初始值
        Double totalSalary = employees.stream().collect(Collectors.reducing(0.0, Employee::getSalary, Double::sum));
        System.out.println(totalSalary);

        // 10、Collectors.groupingBy:分组,接收 Function 函数接口,相同的为一组
        Map<Integer, List<Employee>> ageMap = employees.stream().collect(Collectors.groupingBy(Employee::getAge));
        ageMap.forEach((k,v) -> {
            System.out.println(k + "===>" + v);
        });
        
        // 10.2:自定义分组名称
        Map<String, List<Employee>> ageMap2 = employees.stream().collect(
                Collectors.groupingBy(
                        employee -> {
                            if (employee.getAge() >= 22 && employee.getAge() < 26) {
                                return "22岁-26岁:";
                            } else if (employee.getAge() >= 26 && employee.getAge() < 30) {
                                return "26岁-30岁:";
                            } else {
                                return "30岁以上:";
                            }
                        }
                ));
        ageMap2.forEach((k,v) -> {
            System.out.println(k + "===>" + v);
        });
        

        // 11、Collectors.partitioningBy,分区,接收 断言 Predicate 函数接口,符合条件的分到一起
        // 分区可以认为是特殊的分组,只能分为两个区,一个是符合条件的true区,一个是不符合条件的false区,不能自定义
        Map<Boolean, List<Employee>> map = employees.stream().collect(Collectors.partitioningBy(employee -> employee.getAge() > 25));
        map.forEach((k,v) -> {
            System.out.println(k + "===>" + v);
        });
    }
}
b)案例2:三个参数的收集器,与reduce类似,串行时并不执行参数3,并行时,参数3作为一个汇总
public class StreamMain {

    private static List<Employee> employees = new ArrayList<>();

    static {
        // 创建数据
        employees.add(new Employee(1L,24,"苏小坡",5000.00));
        employees.add(new Employee(2L,24,"苏小由",6000.00));
        employees.add(new Employee(3L,25,"石小敢",6000.00));
        employees.add(new Employee(4L,23,"张小毫",6800.00));
        employees.add(new Employee(5L,23,"李小杰",6800.00));
        employees.add(new Employee(6L,31,"杜小甫",8000.00));
        employees.add(new Employee(7L,29,"辛小疾",8500.00));
    }

    public static void main(String[] args) {
        // parallel():设置为并行,参数3会执行
        HashSet<Employee> hashSet = employees.stream().parallel().collect(
                () -> {
                    System.out.println("参数1----");
                    return new HashSet<>();
                },
                (a, b) -> {
                    // 累加器, a就是要返回的类型,这里是HashSet,b是每一次的值,就是Employee
                    System.out.println("参数2----a:" + a + "b:" + b);
                    // 每个员工涨薪20%
                    b.setSalary(b.getSalary() * 1.2);
                    // 将涨薪后的员工添加到HashSet中,返回
                    a.add(b);
                }
                ,
                (x, y) -> {
                    // 当串行时,此方法不运行
                    System.out.println("参数3----x:" + x + "y:" + y);
                    // x和y都是HashSet,这里做一次合并
                    x.addAll(y);
                });

        hashSet.forEach(System.out::println);
    }
}

5)最大值-max

作用:获取流中最大值

方法定义:

根据提供的Comparator返回此流的最大元素
Optional<T> max(Comparator<? super T> comparator);

案例:获取数组中最大的数

Integer[] arr = {1,2,3,4,5};
Optional<Integer> max = Arrays.stream(arr).max(Integer::compareTo);
System.out.println(max.get());

6)最小值-min

作用:获取流中最小值

方法定义:

根据提供的Comparator返回此流的最小元素
Optional<T> min(Comparator<? super T> comparator);

案例:获取数组中最大的数

Integer[] arr = {1,2,3,4,5};
Optional<Integer> min = Arrays.stream(arr).min(Integer::compareTo);
System.out.println(min.get());

7)元素个数-count

作用:获取流中元素个数

方法定义:

方法返回一个long类型数据
long count();

案例:获取数组中元素个数

Integer[] arr = {1,2,3,4,5};
// 获取长度:5
long count = Arrays.stream(arr).count();
System.out.println(count);

// 过滤后再获取长度:2
long count1 = Arrays.stream(arr).filter(x -> x > 3).count();
System.out.println(count1);

4、短路操作【Short-circuiting】

1)任意匹配-anyMatch

作用:Stream 中只要有一个元素符合传入的 predicate,返回 true;

方法定义:

boolean anyMatch(Predicate<? super T> predicate);

案例:薪资大于8000,如果有符合条件的就直接返回true,不再向下执行

boolean match = employees.stream().anyMatch(employee -> employee.getSalary() > 8000);

2)全部匹配-allMatch

作用:Stream 中全部元素符合传入的 predicate,返回 true;

方法定义:

boolean allMatch(Predicate<? super T> predicate);

案例:薪资是否都大于3000,如果所有元素都符合条件就返回true

boolean match = employees.stream().allMatch(employee -> employee.getSalary() > 3000);

3)无一匹配-noneMatch

作用:Stream 中没有一个元素符合传入的 predicate,返回 true

方法定义:

boolean noneMatch(Predicate<? super T> predicate);

案例:薪资是否有小于3000的,如果都没有则返回true

boolean match = employees.stream().noneMatch(employee -> employee.getSalary() < 3000);

4)第一个元素-findFirst

作用:用于返回满足条件的第一个元素(但是该元素是封装在Optional类中)

方法定义:

Optional<T> findFirst();

案例:获取流中第一个员工

Optional<Employee> first = employees.stream().findFirst();
System.out.println(first.get());

5)任意元素-findAny

作用:返回流中的任意元素(但是该元素也是封装在Optional类中)

方法定义:

Optional<T> findAny();

案例:获取任意一个薪资大于6000的员工名字

Optional<String> any = employees.stream().filter(employee -> employee.getSalary() > 6000).map(Employee::getName).findAny();
System.out.println(any.get());

通过多次执行,findAny每次返回的都是第一个元素,怀疑和findFirst一样,其实不然,findAny()操作,返回的元素是不确定的,对于同一个列表多次调用findAny()有可能会返回不同的值。使用findAny()是为了更高效的性能。如果是数据较少,串行地情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个

比如并行:此时返回的值就不确定,但是少量数据时重复概率还是很大的,可能是因为Java编译器JIT做了优化,快速执行出一个结果

Optional<String> any = employees.parallelStream().filter(employee -> employee.getSalary() > 6000).map(Employee::getName).findAny();
System.out.println(any.get());

6)操作文件

通过java.nio.file.Files对象的lines方法对文件进行流处理

public class StreamMain {

    public static void main(String[] args) {
        // 将文件转换为Path对象
        Path path = new File("D:0-codestt-openstt-01-streamsrcmainjavacomsttstream2stream.txt").toPath();
        try {
            // 使用nio中的Files对象将文件根据行转换为流
            Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8);
            lines.forEach(System.out::println);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:

定风波
苏轼
三月七日,沙湖道中遇雨。
雨具先去,同行皆狼狈,余独不觉,已而遂晴,故作此词。
莫听穿林打叶声,何妨吟啸且徐行。
竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。
料峭春风吹酒醒,微冷,山头斜照却相迎。
回首向来萧瑟处,归去,也无风雨也无晴。

五、Stream使用案例

提前准备:

import java.math.BigDecimal;

public class Waybill {
    // id
    private Long id;
    // 运单编号
    private String wayNo;
    // 运费
    private BigDecimal price;
    // 货物类型
    private String freightType;
    // 距离
    private BigDecimal distance;
    // 目的地
    private String endAddress;

    public Waybill(Long id, String wayNo, BigDecimal price, String freightType, BigDecimal distance, String endAddress) {
        this.id = id;
        this.wayNo = wayNo;
        this.price = price;
        this.freightType = freightType;
        this.distance = distance;
        this.endAddress = endAddress;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getWayNo() {
        return wayNo;
    }

    public void setWayNo(String wayNo) {
        this.wayNo = wayNo;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public String getFreightType() {
        return freightType;
    }

    public void setFreightType(String freightType) {
        this.freightType = freightType;
    }

    public BigDecimal getDistance() {
        return distance;
    }

    public void setDistance(BigDecimal distance) {
        this.distance = distance;
    }

    public String getEndAddress() {
        return endAddress;
    }

    public void setEndAddress(String endAddress) {
        this.endAddress = endAddress;
    }

    @Override
    public String toString() {
        return "Waybill{" +
                "id=" + id +
                ", wayNo='" + wayNo + ''' +
                ", price=" + price +
                ", freightType='" + freightType + ''' +
                ", distance=" + distance +
                ", endAddress='" + endAddress + ''' +
                '}';
    }
}

1、获取运费大于5000元的运单,并放到新集合中

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WaybillMain {
    private static List<Waybill> waybills = new ArrayList<>();

    static {
        // 创建数据
        waybills.add(new Waybill(1L,"Y11111111111",new BigDecimal(1000),"钢材",new BigDecimal(200),"上海市"));
        waybills.add(new Waybill(2L,"Y22222222222",new BigDecimal(2000),"钢材",new BigDecimal(300),"郑州市"));
        waybills.add(new Waybill(3L,"Y33333333333",new BigDecimal(3000),"水泥",new BigDecimal(300),"北京市"));
        waybills.add(new Waybill(4L,"Y44444444444",new BigDecimal(4000),"水泥",new BigDecimal(400),"广州市"));
        waybills.add(new Waybill(5L,"Y55555555555",new BigDecimal(5000),"沙子",new BigDecimal(500),"上海市"));
        waybills.add(new Waybill(6L,"Y66666666666",new BigDecimal(6000),"板材",new BigDecimal(500),"深圳市"));
        waybills.add(new Waybill(7L,"Y77777777777",new BigDecimal(7000),"蔬菜",new BigDecimal(500),"杭州市"));
    }

    public static void main(String[] args) {
        // 1、传统写法
        // 获取运费大于5000元的运单,并放到新集合中
        List<Waybill> newWaybills = new ArrayList<Waybill>();
        for (Waybill waybill : waybills) {
            // 判断价格大于5000,BigDecimal需要使用compareTo方法比较
            // 1:左边比右边大,0:相等,-1:右边比左边大
            if(waybill.getPrice().compareTo(new BigDecimal(5000)) == 1) {
                newWaybills.add(waybill);
            }
        }
        // 遍历
        for (Waybill newWaybill : newWaybills) {
            System.out.println(newWaybill);
        }
        // 2、Stream + Lambda写法
        System.out.println("**华丽丽的分割线**");
        // 1) 通过集合的stream()方法创建流对象
        Stream<Waybill> stream = waybills.stream();
        // 2) 通过流对象的方法计算数据,filter:过滤数据
        // filter接收一个过滤条件,item为当前操作的元素,比较价格是否大于5000,满足条件的过滤出来,放到一个新的Stream对象中
        Stream<Waybill> waybillStream = stream.filter(item -> item.getPrice().compareTo(new BigDecimal(5000)) == 1);
        // 3) 将过滤后的stream转换为新的集合,调用collect方法即可,toList()转换为List集合,toSet转换为Set集合
        List<Waybill> collect = waybillStream.collect(Collectors.toList());
        // 遍历,通过方法引用遍历
        collect.forEach(System.out::println);
    }
}

运行结果:

Stream API使用教程详解插图34

解释:

  • filter():方法就是中间操作,意为过滤符合条件的数据,但是这个数据你还不使用,就先不执行
  • collect():方法是终结操作,意为要将Stream的计算结果转换为一个List集合,Stream认为你要用计算结果了,所以会执行计算,之后保存结果到新的集合中
  • 计算过程Stream中是不存储数据的,没有获取数据的方法

2、将推荐运单按照运费从高到低排序

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WaybillMain {

    private static List<Waybill> waybills = new ArrayList<>();

    static {
        // 创建数据
        waybills.add(new Waybill(1L,"Y11111111111",new BigDecimal(1000),"钢材",new BigDecimal(200),"上海市"));
        waybills.add(new Waybill(2L,"Y22222222222",new BigDecimal(2000),"钢材",new BigDecimal(300),"郑州市"));
        waybills.add(new Waybill(3L,"Y33333333333",new BigDecimal(3000),"水泥",new BigDecimal(300),"北京市"));
        waybills.add(new Waybill(4L,"Y44444444444",new BigDecimal(4000),"水泥",new BigDecimal(400),"广州市"));
        waybills.add(new Waybill(5L,"Y55555555555",new BigDecimal(5000),"沙子",new BigDecimal(500),"上海市"));
        waybills.add(new Waybill(6L,"Y66666666666",new BigDecimal(6000),"板材",new BigDecimal(500),"深圳市"));
        waybills.add(new Waybill(7L,"Y77777777777",new BigDecimal(7000),"蔬菜",new BigDecimal(500),"杭州市"));
    }

    public static void main(String[] args) {
        // 1、升序排序
        List<Waybill> collect1 = waybills.stream().sorted(Comparator.comparing(Waybill::getPrice)).collect(Collectors.toList());
        System.out.println("**升序排序**");
        collect1.forEach(System.out::println);
        // 2、降序排序,调用reversed方法即可降序
        List<Waybill> collect2 = waybills.stream().sorted(Comparator.comparing(Waybill::getPrice).reversed()).collect(Collectors.toList());
        System.out.println("**降序排序**");
        collect2.forEach(System.out::println);

        // 3、如果只想获取运单号
        List<String> collect3 = waybills.stream().sorted(Comparator.comparing(Waybill::getPrice)).map(Waybill::getWayNo).collect(Collectors.toList());
        System.out.println("**降序只获取运单号**");
        collect3.forEach(System.out::println);
        // 4、先按距离,再按运费,通过thenComparing方法做继续排序
        List<Waybill> collect4 = waybills.stream().sorted(Comparator.comparing(Waybill::getDistance).thenComparing(Waybill::getPrice)).collect(Collectors.toList());
        System.out.println("**先按距离再按运费**");
        collect4.forEach(System.out::println);

        // 5、自定义排序
        List<Waybill> collect5 = waybills.stream().sorted((o1, o2) -> {
            // 排序规则:根据货物类型排序,相同的根据距离排序
            if (o1.getFreightType().equals(o2.getFreightType())) {
                return o1.getDistance().compareTo(o2.getDistance());
            } else {
                return o1.getFreightType().compareTo(o2.getFreightType());
            }
        }).collect(Collectors.toList());
        System.out.println("**自定义排序: 根据货物类型排序,相同的根据距离排序**");
        collect5.forEach(System.out::println);
    }
}

运行结果:

Stream API使用教程详解插图36

3、统计最高运费,最低运费,平均运费

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WaybillMain {

    private static List<Waybill> waybills = new ArrayList<>();

    static {
        // 创建数据
        waybills.add(new Waybill(1L,"Y11111111111",new BigDecimal(1000),"钢材",new BigDecimal(200),"上海市"));
        waybills.add(new Waybill(2L,"Y22222222222",new BigDecimal(2000),"钢材",new BigDecimal(300),"郑州市"));
        waybills.add(new Waybill(3L,"Y33333333333",new BigDecimal(3000),"水泥",new BigDecimal(300),"北京市"));
        waybills.add(new Waybill(4L,"Y44444444444",new BigDecimal(4000),"水泥",new BigDecimal(400),"广州市"));
        waybills.add(new Waybill(5L,"Y55555555555",new BigDecimal(5000),"沙子",new BigDecimal(500),"上海市"));
        waybills.add(new Waybill(6L,"Y66666666666",new BigDecimal(6000),"板材",new BigDecimal(500),"深圳市"));
        waybills.add(new Waybill(7L,"Y77777777777",new BigDecimal(7000),"蔬菜",new BigDecimal(500),"杭州市"));
    }

    public static void main(String[] args) {
        // 1、最高运费,通过max方法
        Optional<Waybill> max = waybills.stream().max(Comparator.comparing(Waybill::getPrice));
        System.out.println("**运费最高**");
        System.out.println(max);
        // 2、最低运费,通过min方法
        Optional<Waybill> min = waybills.stream().min(Comparator.comparing(Waybill::getPrice));
        System.out.println("**运费最低**");
        System.out.println(min);
        // 3、平均运费,通过 Collectors.averagingDouble计算平均值,需要将 BigDecimal转换为double类型
        Double avg = waybills.stream().collect(Collectors.averagingDouble(item -> item.getPrice().doubleValue()));
        System.out.println("**平均运费**");
        System.out.println(avg);
    }
}

运行截图:

Stream API使用教程详解插图38

4、将运单按货物类型分类,将运单按货物类型和目的地分类,将运单按照运费是否高于5000元分为两部分

import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class WaybillMain {

    private static List<Waybill> waybills = new ArrayList<>();

    static {
        // 创建数据
        waybills.add(new Waybill(1L,"Y11111111111",new BigDecimal(1000),"钢材",new BigDecimal(200),"上海市"));
        waybills.add(new Waybill(2L,"Y22222222222",new BigDecimal(2000),"钢材",new BigDecimal(300),"郑州市"));
        waybills.add(new Waybill(3L,"Y33333333333",new BigDecimal(3000),"水泥",new BigDecimal(300),"北京市"));
        waybills.add(new Waybill(4L,"Y44444444444",new BigDecimal(4000),"水泥",new BigDecimal(400),"广州市"));
        waybills.add(new Waybill(5L,"Y55555555555",new BigDecimal(5000),"沙子",new BigDecimal(500),"上海市"));
        waybills.add(new Waybill(6L,"Y66666666666",new BigDecimal(6000),"板材",new BigDecimal(500),"深圳市"));
        waybills.add(new Waybill(7L,"Y77777777777",new BigDecimal(7000),"蔬菜",new BigDecimal(500),"杭州市"));
    }

    public static void main(String[] args) {
        // 1、运单按货物类型分类
        Map<String, List<Waybill>> collect1 = waybills.stream().collect(Collectors.groupingBy(Waybill::getFreightType));
        System.out.println("** 运单按货物类型分类 **");
        collect1.forEach((key,value) -> {
            System.out.println("key==>" + key + ",value==>" + value);
        });

        // 2、运单按货物类型和目的地分类
        Map<String, Map<String, List<Waybill>>> collect2 = waybills.stream().collect(Collectors.groupingBy(Waybill::getFreightType, Collectors.groupingBy(Waybill::getEndAddress)));
        System.out.println("** 运单按货物类型和目的地分类 **");
        collect2.forEach((key,value) -> {
            System.out.println("key==>" + key + ",value==>" + value);
        });

        // 3、运单按照运费是否高于5000元分为两部分,这个叫分区了
        Map<Boolean, List<Waybill>> collect3 = waybills.stream().collect(Collectors.partitioningBy(item -> item.getPrice().compareTo(new BigDecimal(5000)) == 1));
        System.out.println("** 运单按照5000分区 **");
        collect3.forEach((key,value) -> {
            System.out.println("key==>" + key + ",value==>" + value);
        });
    }
}

运行截图:

Stream API使用教程详解插图40

六、总结

  • Stream配合Lambda可以非常方便操作计算数据,让冗余的代码变的整洁
  • 尽量不要让sql去做复杂的查询,数据库主要作用是数据存储,复杂查询会降低性能
  • 多数语言中比如Python、Scala等语言中都存在流操作,也有助于掌握其它语言
  • Stream API的方法较多,知道有哪些处理方法,需要使用时可以点进源码看使用说明

发表评论