函数式编程

111 阅读15分钟

github.com/yjiewei/Det… www.exception.site/java8/java8… juejin.cn/post/684490… juejin.cn/post/684490… www.pdai.tech/md/java/jav… juejin.cn/post/684490… blog.csdn.net/weixin_4598… blog.csdn.net/mu_wind/art…

函数式编程-stream流

1、概述

1.1为什么学?

  • 能够看懂代码
  • 大数据量下处理集合效率高(并行流,代替多线程)
  • 代码可读性高
  • 消灭多层嵌套

1.2 函数式编程思想

1.2.1 概念

面向对象思想需要关注用什么对象完成什么事情,而函数式思想就类似于数学中的函数,更关注于对数据进行什么操作。

1.2.2 优点

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

2、lambda表达式

2.1 概述

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

2.2 核心原则

只关注参数和方法体

3、Stream流

3.1 概述

java8的stream使用的是函数式编程模式,如同他的名字一样,他可以被用来对集合数组进行链状流式的操作,可以更方便的让我们对集合或数组操作。流表示包含着一系列元素的集合,可以对其做不同类型的操作,用来对这些元素执行计算

Stream可以由数组或集合创建,对流的操作分为两种:

  1. 中间操作,每次返回一个新的流,可以有多个。
  2. 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值

另外,Stream有几个特性:

  • stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
  • stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
  • stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。

3.2 案例数据准备

@Data
gsConstructor
@AllArgsConstructor
@EqualsAndHashCode // 用于后期的去重使用
public class Author {
    private Long id;
    private String name;
    private  Integer age;
    private  String intro;
    private List<Book> books;

}

@Data
@NoArgsConstructor // 空参构造
@AllArgsConstructor // 有参构造
@EqualsAndHashCode
public class Book {
    private Long id;
    private String name;
    private String category;
    private  Integer score;
    private String intro;
}
public class StreamDemo {
    public static void main(String[] args){
        List<Author> authors = getAuthors();
        authors.stream()  // 把集合转换成stream流
                .distinct()
                .filter((author) -> author.getAge() > 18)
                .forEach(author -> System.out.println(author.getName()));
    }

    public static List<Author> getAuthors(){
        Author author = new Author(1L,"作者1",33,"作者简介1",null);
        Author author2 = new Author(2L,"作者2",33,"作者简介2",null);
        Author author3 = new Author(3L,"作者3",33,"作者简介3",null);
        Author author4 = new Author(4L,"作者4",33,"作者简介4",null);

        // 书籍列表
        List<Book> books1 = new ArrayList<>();
        List<Book> books2 = new ArrayList<>();
        List<Book> books3 = new ArrayList<>();

        books1.add(new Book(1L,"书籍1","哲学",88,"书籍简介1"));
        books1.add(new Book(2L,"书籍2","哲学",88,"书籍简介2"));

        books2.add(new Book(3L,"书籍3","哲学",88,"书籍简介3"));
        books2.add(new Book(3L,"书籍3","哲学",88,"书籍简介3"));
        books2.add(new Book(4L,"书籍4","哲学",88,"书籍简介4"));

        books3.add(new Book(5L,"书籍5","哲学",88,"书籍简介5"));
        books3.add(new Book(6L,"书籍6","哲学",88,"书籍简介6"));
        books3.add(new Book(6L,"书籍6","哲学",88,"书籍简介6"));

        author.setBooks(books1);
        author2.setBooks(books2);
        author3.setBooks(books3);
        author4.setBooks(books3);

        List<Author> authorList = new ArrayList<>();
        authorList.add(author);
        authorList.add(author2);
        authorList.add(author3);
        authorList.add(author4);
        return authorList;

    }
}

3.3 快速入门

3.3.1 需求

调用getAuthors方法获取到作家的集合,现在需要打印所有年龄小于18 的作家的名字,并去重

3.3.2 实现

List<Author> authors = getAuthors();
        authors.stream()  // 把集合转换成stream流
                .distinct()
                .filter((author) -> author.getAge() > 18)
                .forEach(author -> System.out.println(author.getName()));

3.4 常用操作

image.png

3.4.1 创建流

Stream可以通过集合数组创建。

1、通过 java.util.Collection.stream() 方法用集合创建流

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

2、使用java.util.Arrays.stream(T[] array)方法用数组创建流

int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);

3、使用Stream的静态方法:of()、iterate()、generate()

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println);
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);

3.4.2 数据流的执行顺序

中间操作的有个重要特性 —— 延迟性

Stream.of("d2", "a2", "b1", "b3", "c") 
    .filter(s -> { 
    System.out.println("filter: " + s);
    return true;
});

执行此段代码会出现什么结果?您可能会认为,将依次打印 "d2", "a2", "b1", "b3", "c" 元素。然而当你实际去执行的时候,它不会打印任何内容

原因是:当且仅当存在终端操作时,中间操作操作才会被执行。

Stream.of("d2", "a2", "b1", "b3", "c") 
    .filter(s -> { System.out.println("filter: " + s); 
     return true;
     }) 
    .forEach(s -> System.out.println("forEach: " + s));

再次执行,我们会看到输出如下:

filter: d2
forEach: d2 
filter: a2 
forEach: a2 
filter: b1 
forEach: b1 
filter: b3 
forEach: b3 
filter: c 
forEach: c

预想的执行顺序:应该是先将所有 filter 前缀的字符串打印出来,才会打印 forEach 前缀的字符串

事实上,输出的结果却是随着链条垂直移动的,当 Stream 开始处理 d2 元素时,它实际上会在执行完 filter 操作后,再执行 forEach 操作,接着才会处理第二个元素

为什么设计成这样:出于性能的考虑,这样设计可以减少对每个元素的实际操作数

Stream.of("d2", "a2", "b1", "b3", "c") 
    .map(s -> { 
    System.out.println("map: " + s); 
    return s.toUpperCase(); // 转大写 })
    .anyMatch(s -> { 
    System.out.println("anyMatch: " + s); 
    return s.startsWith("A"); 
    // 过滤出以 A 为前缀的元素
 });
    
    // map: d2 
    // anyMatch: D2 
    // map: a2 
    // anyMatch: A2

终端操作 anyMatch()表示任何一个元素以 A 为前缀,返回为 true,就停止循环。所以它会从 d2 开始匹配,接着循环到 a2 的时候,返回为 true ,于是停止循环。

由于数据流的链式调用是垂直执行的,map这里只需要执行两次。相对于水平执行来说,map会执行尽可能少的次数,而不是把所有元素都 map 转换一遍

3.4.3 中间操作顺序的重要性

下面的例子由两个中间操作mapfilter,以及一个终端操作forEach组成。让我们再来看看这些操作是如何执行的:

        Stream.of("d2", "a2", "b1", "b3", "c")
                .map(s -> {
                    System.out.println("map: " + s);
                    return s.toUpperCase(); // 转大写
                })
                .filter(s -> {
                    System.out.println("filter: " + s);
                    return s.startsWith("A"); // 过滤出以 A 为前缀的元素
                })
                .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

// map:     d2
// filter:  D2
// map:     a2
// filter:  A2
// forEach: A2
// map:     b1
// filter:  B1
// map:     b3
// filter:  B3
// map:     c
// filter:  C

mapfilter会对集合中的每个字符串调用五次,而forEach却只会调用一次,因为只有 "a2" 满足过滤条件

如果我们改变中间操作的顺序,将filter移动到链头的最开始,就可以大大减少实际的执行次数:

       Stream.of("d2", "a2", "b1", "b3", "c")
                .filter(s -> {
                    System.out.println("filter: " + s);
                    return s.startsWith("a"); // 过滤出以 a 为前缀的元素
                })
                .map(s -> {
                    System.out.println("map: " + s);
                    return s.toUpperCase(); // 转大写
                })
                .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

// filter:  d2
// filter:  a2
// map:     a2
// forEach: A2
// filter:  b1
// filter:  b3
// filter:  c

现在,map仅仅只需调用一次,性能得到了提升,这种小技巧对于流中存在大量元素来说,是非常很有用的

接下来,对上面的代码再添加一个中间操作sorted

Stream.of("d2", "a2", "b1", "b3", "c")
      .sorted((s1, s2) -> {
          System.out.printf("sort: %s; %s\n", s1, s2);
          return s1.compareTo(s2); // 排序
      })
      .filter(s -> {
          System.out.println("filter: " + s);
          return s.startsWith("a"); // 过滤出以 a 为前缀的元素
      })
      .map(s -> {
          System.out.println("map: " + s);
          return s.toUpperCase(); // 转大写
      })
      .forEach(s -> System.out.println("forEach: " + s)); // for 循环输出

sorted 是一个有状态的操作,因为它需要在处理的过程中,保存状态以对集合中的元素进行排序。

执行上面代码,输出如下:

sort:    a2; d2
sort:    b1; a2 
sort:    b1; d2
sort:    b1; a2
sort:    b3; b1
sort:    b3; d2
sort:    c; b3
sort:    c; d2
filter:  a2
map:     a2
forEach: A2
filter:  b1
filter:  b3
filter:  c
filter:  d2

sorted是水平执行的。因此,在这种情况下,sorted会对集合中的元素组合调用八次。这里,我们也可以利用上面说道的优化技巧,将 filter 过滤中间操作移动到开头部分:

Stream.of("d2", "a2", "b1", "b3", "c")
                .filter(s -> {
                    System.out.println("filter: " + s);
                    return s.startsWith("a");
                })
                .sorted((s1, s2) -> {
                    System.out.printf("sort: %s; %s\n", s1, s2);
                    return s1.compareTo(s2);
                })
                .map(s -> {
                    System.out.println("map: " + s);
                    return s.toUpperCase();
                })
                .forEach(s -> System.out.println("forEach: " + s));

// filter:  d2
// filter:  a2
// filter:  b1
// filter:  b3
// filter:  c
// map:     a2
// forEach: A2

可以看出 sorted从未被调用过,因为经过filter过后的元素已经减少到只有一个,这种情况下,是不用执行排序操作的。因此性能被大大提高了

3.4.3 中间操作

of

可接收一个泛型对象或可变成泛型集合,构造一个 Stream 对象。

private static void createStream(){
    Stream<String> stringStream = Stream.of("a","b","c");
}
empty

创建一个空的 Stream 对象。

concat

连接两个 Stream ,不改变其中任何一个 Steam 对象,返回一个新的 Stream 对象

private static void concatStream(){
    Stream<String> a = Stream.of("a","b","c");
    Stream<String> b = Stream.of("d","e");
    Stream<String> c = Stream.concat(a,b);
}

Optional对象 它代表一个可能存在也可能不存在的值。如果Stream为空,那么该值不存在,如果不为
空,则该值存在。通过调用get方法可以取出Optional对象中的值

Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
Optional<Integer> max  = integerStream.max((o1, o2) -> o1 - o2);
System.out.println(max.get());

传给它一个Comparator对象。Java8提供了一个新的静态方法comparing,使用它可以方便地实现一个比较器

// 例2
List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
Optional<String> max1 = list.stream().max((o1, o2) -> o1.length() - o2.length());
Optional<String> max2 = list.stream().max(Comparator.comparing(String::length));
filter

用于条件筛选过滤,筛选出符合条件的数据
举例:筛选出年龄大于 18 的记录

List<Author> authors = getAuthors();
authors.stream()  // 把集合转换成stream流
        .distinct()
        .filter((author) -> author.getAge() > 18)
        .forEach(author -> System.out.println(author.getName()));
map(常用)

可以对流中的元素进行计算或转换
map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
例如:打印所有作家姓名
把作家的姓名拿出来作为流返回;流里还是4个元素,但是类型变成字符串类型

authors.stream()
        .map(Author::getName)
        .forEach(System.out::println);

对流中元素进行计算

authors.stream()
        .map(Author::getAge)
        .map(age -> age+10)
        .forEach(System.out::println);
distinct

可以去除流中重复的对象
例如:打印所有的作家名称,不能有重复元素

List<Author> authors = getAuthors();
authors.stream().distinct()
        .forEach(System.out::println);

注意:distinct方法是依靠Object的equals方法来判断是否是相同对象的,所以需要注意重写equal方法

sorted

Sorted 同样是一个中间操作,它的返参是一个 Stream 流,有两个重载,一个无参数,另外一个有个 Comparator类型的参数。 image.png
无参:
无参类型的按照自然顺序进行排序,只适合比较单纯的元素,比如数字、字母等。
注意:如果调用空参的sorted()方法,需要流中的元素是实现了Comparable

image.png
有参

// 对流中的元素按照年龄进行降序排序,并且要求不能有重复元素
List<Author> authors = getAuthors();
authors.stream()
        .distinct()
        .sorted(( o1,o2) ->  o2.getAge() - o1.getAge())
        .forEach(System.out::println);
limit

获取前 n 条数据,类似于 MySQL 的limit,只不过只能接收一个参数,就是数据条数。

// 对流中的元素按照年龄进行降序排序,并且要求不能有重复元素,然后打印其中年龄最大的亮哥作家的姓名
List<Author> authors = getAuthors();
authors.stream()
        .distinct()
        .sorted(( o1,o2) ->  o2.getAge() - o1.getAge())
        .limit(2)
        .forEach(author -> System.out.println(author.getName()));
skip

跳过前 n 条数据,例如下面代码,返回结果是 c。

List<Author> authors = getAuthors();
authors.stream()
        .distinct()
        .sorted(( o1,o2) ->  o2.getAge() - o1.getAge())
        .skip(1)
        .forEach(author -> System.out.println(author.getName()));
flatMap

map操作, 将流中的对象转换为另一种类型。但是,Map只能将每个对象映射到另一个对象,FlatMap 能够将流的每个元素, 转换为其他对象的流。因此,每个对象可以被转换为零个,一个或多个其他对象,将流中的每个值都换成另一个流,然后把所有流连接成一个流。之后,这些流的内容会被放入flatMap返回的流中。
例1:
打印所有书籍名字,要求对重复元素进行去重

// 打印所有书籍名字,要求对重复元素进行去重
List<Author> authors = getAuthors();
authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .distinct()
        .forEach(book -> System.out.println(book.getName()));

例2:
打印现有数据的所有分类,要求对分类进行去重

// 打印现有数据的所有分类,要求对分类进行去重
List<Author> authors = getAuthors();
authors.stream()
        .flatMap(author -> author.getBooks().stream())
        // 对书籍进行去重
        .distinct()
        // 数组转换成流对象
        .flatMap(book -> Arrays.stream(book.getCategory().split(",")))
        // 对分类进行去重
        .distinct()
        .forEach(System.out::println);

3.4.4 终结操作

foreach

对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体操作 ,和 peek 方法类似,都接收一个消费者函数式接口,可以对每个元素进行对应的操作,但是和 peek 不同的是,forEach 执行之后,这个 Stream 就真的被消费掉了,之后这个 Stream 流就没有了,不可以再对它进行后续操作了,而 peek操作完之后,还是一个可操作的 Stream 对象

例:输出所有作家的姓名

// 输出所有作家的姓名
List<Author> authors = getAuthors();
authors.stream()
        .map(Author::getName)
        .forEach(System.out::println);
count

返回流当中元素个数。

Stream<String> a = Stream.of("a", "b", "c");
long x = a.count();
min/max

一般用于求数字集合中的最大值,或者按实体中数字类型的属性比较,拥有最大值的那个实体。它接收一个 Comparator<T>,它是一个函数式接口类型,专门用作定义两个对象之间的比较,例如下面这个方法使用了 Integer::compareTo这个方法引用。

// 分别获取这些作家的所出书籍的最高分和最低分并打印
List<Author> authors = getAuthors();
Optional<Integer> max = authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .map(Book::getScore)
        .max((o1, o2) -> o1 - o2);
System.out.println(max.get());
collect

把当前流转换成一个集合
collect 是一个非常有用的终端操作,它可以将流中的元素转变成另外一个不同的对象,例如一个ListSetMap。collect 接受入参为Collector(收集器),collect主要依赖java.util.stream.Collectors类内置的静态方法

collect\Collector\Collectors区别与关联
1️ 、 collect是Stream流的一个 终止方法,会使用传入的收集器(入参)对结果执行相关的操作,这个收集器必须是Collector接口的某个具体实现类
2️ 、 Collector是一个 接口,collect方法的收集器是Collector接口的 具体实现类
3️ 、 Collectors是一个 工具类,提供了很多的静态工厂方法, 提供了很多Collector接口的具体实现类,是为了方便程序员使用而预置的一些较为通用的收集器(如果不使用Collectors类,而是自己去实现Collector接口,也可以)。
image.png

image.png

归集(toList/toSet/toMap)

所谓恒等处理,指的就是Stream的元素在经过Collector函数处理前后完全不变,例如toList()操作,只是最终将结果从Stream中取出放入到List对象中,并没有对元素本身做任何的更改处理:

list.stream().collect(Collectors.toList());
list.stream().collect(Collectors.toSet());
list.stream().collect(Collectors.toCollection());

image.png 例子: 获取一个存放所有作者名字的list集合

//获取一个存放所有作者名字的list集合
List<Author> authors = getAuthors();
List<String> nameList = authors.stream()
        .map(Author::getName)
        // Collectors工具类
        .collect(Collectors.toList());
System.out.println(nameList);

获取一个所有书名的Set集合

// 获取一个所有书名的Set集合
Set<String> bookNameList = authors.stream()
        .flatMap(author -> author.getBooks().stream())
        .map(Book::getName)
        .collect(Collectors.toSet());
System.out.println(bookNameList);

获取一个map集合,map的key为作者名,value为List

// 获取一个map集合,map的key为作者名,value为List<Book>
Map<String, List<Book>> map = authors.stream()
        .distinct()
        .collect(Collectors.toMap(new Function<Author, String>() {
            @Override
            public String apply(Author author) {
                return author.getName();
            }
        }, new Function<Author, List<Book>>() {
            @Override
            public List<Book> apply(Author author) {
                return author.getBooks();
            }
        }));
统计(count/averaging)

Collectors提供了一系列用于数据统计的静态方法:
对于归约汇总类的操作,Stream流中的元素逐个遍历,进入到Collector处理函数中,然后会与上一个元素的处理结果进行合并处理,并得到一个新的结果,以此类推,直到遍历完成后,输出最终的结果

image.png

    public void calculateSum() {
    Integer salarySum = getAllEmployees().stream()
            .filter(employee -> "上海公司".equals(employee.getSubCompany()))
            .collect(Collectors.summingInt(Employee::getSalary));
    System.out.println(salarySum);
}
  • 计数:count
  • 平均值:averagingInt、averagingLong、averagingDouble
  • 最值:maxBy、minBy
  • 求和:summingInt、summingLong、summingDouble
  • 统计以上所有:summarizingInt、summarizingLong、summarizingDouble
// 求总数
Long count = authors.stream().collect(Collectors.counting());
// 求平均年龄
Double average = authors.stream().collect(Collectors.averagingInt(Author::getAge));
// 求最高年龄
Optional<Integer> maxAge = authors.stream().map(Author::getAge).collect(Collectors.maxBy(Integer::compare));
// 求年龄和
Integer sumAge = authors.stream().collect(Collectors.summingInt(Author::getAge));
// 一次性统计所有信息
DoubleSummaryStatistics collect1 = authors.stream().collect(Collectors.summarizingDouble(Author::getAge));

image.png

分组(partitioningBy/groupingBy)
  • 分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。
  • 分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。

Collectors工具类中提供了groupingBy方法用来得到一个分组操作Collector,其内部处理逻辑可以参见下图的说明:

有一个方法只需要传入一个分组函数即可,这是因为其默认使用了toList()作为值收集器 image.png
如果不仅需要分组,还需要对分组后的数据进行处理的时候,则需要同时给定分组函数以及值收集器,其内又传入了一个归约汇总Collector Collectors.summarizingInt() image.png

企业微信截图_16665982942644.png

// 分组(partitioningBy/groupingBy)
Map<Boolean, List<Integer>> part  = authors.stream().map(Author::getAge).collect(Collectors.partitioningBy(age -> age > 30));
// 按照姓名分组
Map<String, List<Author>> group  = authors.stream().collect(Collectors.groupingBy(Author::getName));
// 按照姓名分组,再按年纪分组
Map<String, Map<Integer, List<Author>>> group2 = authors.stream().collect(Collectors.groupingBy(Author::getName, Collectors.groupingBy(Author::getAge)));

image.png

接合(joining)

image.png

// 用逗号所有员工的姓名
String allName = authors.stream().map(Author::getName).collect(Collectors.joining(","));

image.png

reduce归并

image.png
对流中的数据按照你指定的计算方式算出一个结果
reduce的作用是把stream中的元素组合起来,我们可以传入一个初始值,他会按照我们的计算方式依次拿出流中的元素和在初始值的基础上进行计算,计算结果在和后面的元素计算。
它的作用是每次计算的时候都用到上一次的计算结果,比如求和操作,前两个数的和加上第三个数的和,再加上第四个数,一直加到最后一个数位置,最后返回结果,就是 reduce的工作过程

reduce两个参数的重载形式内部计算方式如下:  
    // 初始值
    T result = identity;
    for(T element:this stream){
        result = accumulator.apply(result,element)
    return result;

其中identity 就是我们可以通过方法参数传入的初始值,accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。
例子:
使用reduce 求所有的作者年龄的和

Integer reduce = authors.stream()
     .distinct()
     .map(Author::getAge)
     .reduce(0, Integer::sum);

使用reduce 求所有作者中年龄的最大值

Integer reduce1 = authors.stream()
        .map(Author::getAge)
        .reduce(Integer.MIN_VALUE, (result, element) -> result < element ? element : result);
System.out.println(reduce1);

reduce一个参数的重载形式内部计算方式如下

     boolean foundAny = false;
     T result = null;
     for (T element : this stream) {
         if (!foundAny) {
             foundAny = true;
             result = element;
         }
         else
             result = accumulator.apply(result, element);
     }
     return foundAny ? Optional.of(result) : Optional.empty();

将第一个元素作为参数返回值(将第一个元素作为初始化值)

// 一个参数
Optional<Integer> reduce4 = authors.stream()
        .distinct()
        .map(Author::getAge)
        .reduce(new BinaryOperator<Integer>() {
            @Override
            public Integer apply(Integer result, Integer element) {
                return result > element ? element : result;
            }
        });
reduce4.ifPresent(System.out::println);