Stream流式编程的操作

发布时间 2023-10-23 09:45:58作者: 每天--自然醒

前言:为什么要学函数式编程?大家如果有进入公司实习或者看GitHub上著名项目后

会发现它们都有一个特点:很多地方使用了函数式编程,因此代码会很整洁,可读性也很高。这里我们举个例子:

这里我们想“查询未成年作家的评分在70以上的书籍,同时由于作家和书籍可能出现重复,所以需要进行去重”。因此我们没学函数式编程前会这样写代码:

List<Book> bookList = new ArrayList<>();
​
​
​
Set<Book> uniqueBookValues = new HashSet<>();
​
​
​
Set<Author> uniqueAuthorValues = new HashSet<>();
​
​
​
for (Author author : authors) {
​
​
​
    if (uniqueAuthorValues.add(author)) {
​
​
​
        if (author.getAge() < 18) {
​
​
​
            List<Book> books = author.getBooks();
​
​
​
            for (Book book : books) {
​
​
​
                if (book.getScore() > 70) {
​
​
​
                    if (uniqueBookValues.add(book)) {
​
​
​
                        bookList.add(book);
​
​
​
                    }
​
​
​
                }
​
​
​
            }
​
​
​
        }
​
​
​
    }
​
​
​
}
​
​
​
System.out.println(bookList);

可以看到,这里由于我们的需求,导致出现了“嵌套地狱”,因此阅读起来十分麻烦!但是,如果我们使用了函数式编程后,代码会变成怎么样呢?如下:

List<Book> collect = authors.stream()
​
​
​
    .distinct()
​
​
​
    .filter(author -> author.getAge() < 18)
​
​
​
    .map(author -> author.getBooks())
​
​
​
    .flatMap(Collection::stream)
​
​
​
    .filter(book -> book.getScore() > 70)
​
​
​
    .distinct()
​
​
​
    .collect(Collectors.toList());
​
​
​
System.out.println(collect);

可见,代码量肉眼可见的减少了,也变得美观,可读性也大大提升,因此这就是我们要学习函数式编程的原因!

 

  1. 函数式编程思想

1.1 什么是函数式编程?它的概念是什么?

拿我们之前学过的面向对象编程思想来说:

面向对象编程是“关注我们把哪些东西封装到一个类里面,然后用这个对象做了什么事情”。而函数式编程是“只关注我们对数据做了什么事情,进行了哪些操作”。(这里如果不太理解的话,接下来的Lambda表达式就是很好的例子,大家耐心往下看)

1.2 函数式编程的优点

这里我们上文也说了,其优点有:

  • 代码简洁,开发快速

  • 接近自然语言,易于理解

  • 易于"并发编程"

 

  1. Lambda表达式

2.1 Lambda表达式的描述

Lambda表达式是JDK8中的一个语法格式,它能够对以匿名类为参数的方法进行语法简化,是函数式编程思想的一个重要体现,也是我们接下来理解函数式编程,简化代码的一个重要知识。它让我们不用关注是什么对象。而是更关注我们对数据进行了什么操作。

2.2 基本格式

语法:(重写方法的参数列表)-> { 代码 }

注意!Lambda只能在对同时满足以下条件的代码进行简化:

①是某个接口的匿名内部类

②只有一个重载方法

看到上述语法,大家可能不太理解,确实有点抽象,因此我们这里举几个例子:

例一:

不使用Lambda表达式创建线程时的写法:

new Thread(new Runnable() {
​
​
​
    @Override
​
​
​
    public void run() {
​
​
​
        System.out.println("CUG能不能别做早操了,球球了");
​
​
​
    }
​
​
​
}).start();

使用Lambda表达式的写法:

new Thread(()->{ System.out.println("CUG能不能别做早操了,球球了");
​
​
​
            }
​
​
​
).start();

例二:

不使用Lambda表达式时,我们在某个集合的sort方法里面实现Comparator接口的匿名内部类如下:

        List<Author> authors = getAuthors();
​
​
​
        authors.sort(new Comparator<Author>() {
​
​
​
            @Override
​
​
​
            public int compare(Author o1, Author o2) {
​
​
​
                return 0;
​
​
​
            }
​
​
​
        });

使用Lambda表达式对上述代码进行优化后:

        List<Author> authors = getAuthors();
​
​
​
        authors.sort(((Author o1, Author o2) -> {
​
​
​
            return o1.getAge()-o2.getAge();
​
​
​
        }));

可见,这里就和我们上文说的:只关注实现的方法做了什么,而不去关注我们创建的类是什么、方法名叫什么。并且代码量也肉有可见的减少了,看来也更加美观,如果学会Lambda后,阅读起来也更加方便,可读性更高!

但是,大家会发现我们上文举例子中的Lambda表达式,比如例二,Idea会提示我们还可以优化成下面这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.sort((o1, o2) -> o1.getAge()-o2.getAge());

看到这,大家就纳闷了,tnnd,怎么这么奇怪,怎么一下少了这么多东西,不对劲!不对劲起来了!这里我们就不得不提一下Lambda表达式的省略规则了:

  • 参数类型可以省略

  • 方法体只有一句代码时大括号return和唯一一句代码的分号可以省略

  • 方法只有一个参数时小括号可以省略

大家看完省略规则,再去对比例二和我们Idea帮忙改的代码后,就会理解了!

 

  1. Stream流

3.1 概述

Stream是Java8的一个新特性,使用的是函数式编程模式,如同它的名字一样,它可以被用来对集合或数组进行链状流式的操作。可以更方便的让我们对集合或数组操作。

为什么要用Stream流,大家可以回到文章顶部对比一下那两段代码:一个是我们平时正常写代码,不仅量大,不美观,还出现了“嵌套地狱”,阅读起来十分难受。另一个则使用了Stream流来处理我们需要的数据。

那么,Stream流怎么得到呢?它的操作有哪些?大家别急,我们接下来慢慢说。

3.2 常用操作

一般我们对流操作的三要素:①创建流 -> ②中间操作 -> ③终结操作

可以这样理解:要操作流,那肯定要创建流,然后对流进行一系列的中间操作(比如去重、排序),最后对流进行终结操作(比如输出、查找)。

3.2.1 创建Stream流

我们用到Stream流处理数据时,一般会对如下三种数据类型进行操作:

①单列集合

语法:

集合对象.stream()

比如:

        List<Author> authors = getAuthors();
​
​
​
        //通过集合对象.stream()获取集合对象的stream流
​
​
​
        Stream<Author> stream = authors.stream();

②数组

语法:

Arrays.stream(数组名) `或者使用` Stream.of(数组名)`来创建

比如:

        Integer[] arr = {1,2,3,4,5};
​
​
​
        //通过Arrays.stream(数组名)获取数组的stream流
​
​
​
        Stream<Integer> stream = Arrays.stream(arr);
​
​
​
        //通过Stream.of(数组名)获取数组的stream流
​
​
​
        Stream<Integer> stream2 = Stream.of(arr);

③双列集合(比如Map集合):

目前来说还没有提供双列集合直接转成对应Stream流的方法,因此我们将其转换成单列集合后再创建Stream流。

比如这里我们通过将Map集合的entry转换成Set集合,再来获取流:

        Map<String,Integer> map = new HashMap<>();
​
​
​
        map.put("蜡笔小新",19);
​
​
​
        map.put("黑子",17);
​
​
​
        map.put("日向翔阳",16);
​
​
​
 
​
​
​
        Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();

OK,现在我们学会如何获取Stream流后,应该来学学如何对Stream流进行操作了,也就是中间操作。(接下来操作都会使用作者举的例子:authors.stream())

3.2.2 中间操作

①filter:可以对流中的元素进行条件过滤,符合过滤条件的才能继续留在流中。

比如:我们想要获取并打印所有成年(年龄不小于18)作家的名字,代码如下:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                //通过filter过滤掉不满足条件的数据(保留满足条件的)
​
​
​
                .filter(new Predicate<Author>() {
​
​
​
                    @Override
​
​
​
                    public boolean test(Author author) {
​
​
​
                        return author.getAge()>=18;
​
​
​
                    }
​
​
​
                })
​
​
​
                .forEach(new Consumer<Author>() {
​
​
​
                    @Override
​
​
​
                    public void accept(Author author) {
​
​
​
                        System.out.println(author.getName());
​
​
​
                    }
​
​
​
                });

这里我们复习一下Lambda表达式,对上述代码简化后如下:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                //通过filter过滤掉不满足条件的数据(保留满足条件的)
​
​
​
                .filter(author -> author.getAge() >= 18)
​
​
​
                .forEach(author -> System.out.println(author.getName()));

(接下来我们的所有案例都直接用Lambda表达式优化后的代码)

②map:用于对流中的元素进行计算或转换。

对于转换这个功能来说,比如:我们想将authors.stream()流里面的数据转成装着age数据的流,我们可以这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .map(author -> author.getAge())
​
​
​
                .forEach(age -> System.out.println(age));

这里我们通过跟踪代码来看一下:

img

从Idea的提示来看,在经过Map操作后,Stream流中的数据类型从Author变为了Integer。可见,Map是可以将Stream流中的数据进行类型转换的,转换的类型取决于你返回的数据类型(比如我们上图返回的是age,因此是Integer类型)。

对于转换这个功能来说,比如我们想在上述代码的基础上,给每个作家的年龄再加上10,我们可以这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .map(author -> author.getAge())
​
​
​
                .map(age->age+10)
​
​
​
                .forEach(age -> System.out.println(age));

③distinct:用来去除流中重复的数据。

例如我们想去除作家流中所有重复的数据,并且打印去重后所有作家的名字,可以这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .distinct()
​
​
​
                .forEach(author -> System.out.println(author.getName()));

④sorted:对流中所有元素进行排序

在使用sorted()之前,我们需要回顾一下:之前我们使用Collections工具类中的sort函数时,除了Java指定的几个基本类外(比如Integer、Double、Float),我们对其他类排序的时候都需要让它们继承Comparable接口并重写方法或者在sort里面new一个Compartor的匿名内部类。这里很好理解为什么,因为正常的一些整型数据,计算机肯定知道如何为它们排序,但是如果是我们自定义的类型(比如Author类),那计算机怎么知道它们如何排序呢?所以这里sorted方法有两种用法:

a.调用空参的sorted()方法,需要流中的元素是实现了Comparable。

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

b.调用sorted()方法,并且在()内部new 一个比较器(Compartor)的内部类。

        List<Author> authors = getAuthors();
​
​
​
//        对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
​
​
​
        authors.stream()
​
​
​
                .distinct()
​
​
​
                .sorted(new Comparator<Author>() {
​
​
​
                    @Override
​
​
​
                    public int compare(Author o1, Author o2) {
​
​
​
                        return o1.getAge()-o2.getAge();
​
​
​
                    }
​
​
​
                })
​
​
​
                .forEach(author -> System.out.println(author.getAge()));

用Lambda表达式简化上式后变为:

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

⑤limit:可以设置流的最大长度,超出的部分将被抛弃。

比如我们的作家流,我们只想获取前两个数据,那么可以这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .limit(2)
​
​
​
                .forEach(author -> System.out.println(author.getAge()));

注意:limit只会去掉超出长度后的数据!比如流有十条数据,我们设置limit(5),便会去除后5条。但是如果我们设置limit(11),就不会去除数据。

⑥skip:跳过流中的前n个元素,返回剩下的元素。

比如我们要跳过作家流的前两个数据,可以这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .skip(2)
​
​
​
                .forEach(author -> System.out.println(author.getAge()));

⑦flatMap:可以把一个对象转换成多个对象作为流中的元素。

map只能把一个对象转换成另一个对象来作为流中的元素。而flatMap可以把一个对象转换成多个对象作为流中的元素。

上述这句话怎么理解呢,我们先来看一下作家类的结构:

img

我们想获取所有作家的所有作品的流,那么我们肯定会想到用之前的Map,将作家流转成作品流,如下:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .map(author -> author.getBooks())
​
​
​
                .forEach(books -> System.out.println(books));

但是可以发现,在Idea给我们的提示中,这里map转换后,流中的数据类型并不是我们想要的Book,而是List<Book>:

img  

遇到这种情况,那就无法继续用map来转换了,因此我们要用新的中间操作“flatMap

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .flatMap(new Function<Author, Stream<?>>() {
​
​
​
                    @Override
​
​
​
                    public Stream<?> apply(Author author) {
​
​
​
                        return author.getBooks().stream();
​
​
​
                    }
​
​
​
                })
​
​
​
                .forEach(books -> System.out.println(books));

使用Lambda表达式简化后:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .flatMap((Function<Author, Stream<?>>) author -> author.getBooks().stream())
​
​
​
                .forEach(books -> System.out.println(books));

可以看到,flatMap的返回值是Stream流,从Idea的提示也可以看出,此时流中的数据类型变为了Stream<Object>:

img

虽然显示的是Stream<Object>,但是我们都知道,这里实际上存储的是Stream<Book>,只不过是利用了多态来存储!

好了,以上是我们全部会使用的中间操作,中间操作是用来根据需求处理流中的数据的,那么处理完后,我们要使用流中的数据,也就是我们接下来要说的:“终结操作”。

3.2.2 终结操作

①forEach:对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体操作。

这个操作大家应该不陌生,因为我们上面每个例子最后都是用forEach来使用我们对流处理后的数据的。因此我们这里不做过多介绍,直接来举个例子:我们要输出作家流中所有人的名字可以这样:

        List<Author> authors = getAuthors();
​
​
​
        authors.stream()
​
​
​
                .forEach(author -> System.out.println(author.getName()));

②count:可以用来获取当前流中元素的个数。

作用就是我们获取流,并且对流中的数据进行处理后(中间操作),获取剩下流中元素的个数。

比如我们想获取不重复作家的个数,可以这样:

        List<Author> authors = getAuthors();
​
​
​
        //count是不重复作家的个数
​
​
​
        long count = authors.stream()
​
​
​
                .distinct()
​
​
​
                .count();

③max、min:用来获取流中的最大、最小值。

比如我们想获取作家中所有书籍评分的最大和最小值,并且打印出来:

//        分别获取这些作家的所出书籍的最高分和最低分并打印。
​
​
​
        //Stream<Author>  -> Stream<Book> ->Stream<Integer>  ->求值
​
​
​
 
​
​
​
        List<Author> authors = getAuthors();
​
​
​
        Optional<Integer> max = authors.stream()
​
​
​
                .flatMap(author -> author.getBooks().stream())
​
​
​
                .map(book -> book.getScore())
​
​
​
                .max((score1, score2) -> score1 - score2);
​
​
​
 
​
​
​
        Optional<Integer> min = authors.stream()
​
​
​
                .flatMap(author -> author.getBooks().stream())
​
​
​
                .map(book -> book.getScore())
​
​
​
                .min((score1, score2) -> score1 - score2);
​
​
​
        System.out.println(max.get());
​
​
​
        System.out.println(min.get());

这里可以看到,我们是分别获取了两个流来求最值的,因为这里有个注意事项:一个流只能有一个终结操作,终结操作后就不能继续操作流了

④collect:把当前流转换成一个集合。

比如我们要获取一个存放所有作者名字的List集合:

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

比如我们要获取一个所有书名的Set集合:

//        获取一个所有书名的Set集合。
​
​
​
        List<Author> authors = getAuthors();
​
​
​
        Set<Book> books = authors.stream()
​
​
​
                .flatMap(author -> author.getBooks().stream())
​
​
​
                .collect(Collectors.toSet());
​
​
​
 
​
​
​
        System.out.println(books);

比如我们要获取一个Map集合,map的key为作者名,value为List<Book>

//        获取一个Map集合,map的key为作者名,value为List<Book>
​
​
​
        List<Author> authors = getAuthors();
​
​
​
 
​
​
​
        Map<String, List<Book>> map = authors.stream()
​
​
​
                .distinct()
​
​
​
                .collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
​
​
​
 
​
​
​
        System.out.println(map);

⑥anyMatch:可以用来判断是否有任意符合匹配条件的元素,结果为boolean类型。

比如判断流中是否有年龄在29以上的作家:

//        判断是否有年龄在29以上的作家
​
​
​
        List<Author> authors = getAuthors();
​
​
​
        boolean flag = authors.stream()
​
​
​
                .anyMatch(author -> author.getAge() > 29);
​
​
​
        System.out.println(flag);

⑦allMatch:可以用来判断是否都符合匹配条件,结果为boolean类型。如果都符合结果为true,否则结果为false。

比如判断是否作家都是成年人:

        List<Author> authors = getAuthors();
​
​
​
        boolean b = authors.stream()
​
​
​
                .allMatch(author -> author.getAge()>=18);
​
​
​
        System.out.println(b);

⑧noneMatch:可以判断流中的元素是否都不符合匹配条件。如果都不符合结果为true,否则结果为false。

比如判断作家中是不是所有人年龄都没超过100岁:

//        判断作家是否都没有超过100岁的。
​
​
​
        List<Author> authors = getAuthors();
​
​
​
 
​
​
​
        boolean b = authors.stream()
​
​
​
                .noneMatch(author -> author.getAge() > 100);
​
​
​
        System.out.println(b);

⑨findAny:获取流中的任意一个元素。因为是随机获取!所以该方法没有办法保证获取的一定是流中的第一个元素。

比如我们随机获取一个年龄大于18的作家,如果存在就输出他的名字:

//        获取任意一个年龄大于18的作家,如果存在就输出他的名字
​
​
​
        List<Author> authors = getAuthors();
​
​
​
        Optional<Author> optionalAuthor = authors.stream()
​
​
​
                .filter(author -> author.getAge()>18)
​
​
​
                .findAny();
​
​
​
 
​
​
​
        optionalAuthor.ifPresent(author -> System.out.println(author.getName()));

这里我们用到了Optional这个类,不懂的同学可以去学一下。大家看他的方法ifPresent也应该能猜出来内部帮我们判断了一下:如果我们获取的数据存在则输出,不存在则不输出。

⑩findFirst:获取流中的第一个元素。

比如我们获取一个年龄最小的作家,并输出他的姓名:

//        获取一个年龄最小的作家,并输出他的姓名。
​
​
​
        List<Author> authors = getAuthors();
​
​
​
        Optional<Author> first = authors.stream()
​
​
​
                .sorted((o1, o2) -> o1.getAge() - o2.getAge())
​
​
​
                .findFirst();
​
​
​
 
​
​
​
        first.ifPresent(author -> System.out.println(author.getName()));

终结操作之疑难点——reduce:对流中的数据按照你指定的计算方式计算出一个结果。

reduce的作用是把stream中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素计算。

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

T result = identity;
​
​
​
for (T element : this stream)
​
​
​
    result = accumulator.apply(result, element)
​
​
​
return result;

其中identity就是我们可以通过方法参数传入的初始值,accumulator的apply具体进行什么计算也是我们通过方法参数来确定的。

比如我们使用reduce求所有作家年龄的和:

//        使用reduce求所有作者年龄的和
​
​
​
        List<Author> authors = getAuthors();
​
​
​
        Integer sum = authors.stream()
​
​
​
                .distinct()
​
​
​
                .map(author -> author.getAge())
​
​
​
                .reduce(0, (result, element) -> result + element);
​
​
​
        System.out.println(sum);

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();

如果用一个参数的重载方法去求所有作家的年龄之和代码如下:

        List<Author> authors = getAuthors();
​
​
​
        Optional<Integer> reduce = authors.stream()
​
​
​
                .map(author -> author.getAge())
​
​
​
                .reduce((result, age) -> result + age);
​
​
​
        reduce.ifPresent(System.out::println);

可见,这二者的区别就是:两个参数的我们可以赋一个初始值;而一个参数的初始值则没有,程序会将流中第一个元素的值赋给它,然后再与流之后的元素进行计算。

 

好了,以上是我们流操作所有的方法,这里有一些注意事项大家需要知道一下:

  • 惰性求值(如果没有终结操作,没有中间操作是不会得到执行的)

  • 流是一次性的(一旦一个流对象经过一个终结操作后。这个流就不能再被使用)

  • 不会影响原数据(我们在流中可以多数据做很多处理。但是正常情况下是不会影响原来集合中的元素的。这往往也是我们期望的)