Java 函数式编程(二):函数式数据处理:基本用法

1. Java 8 给 Collection 接口增加的两个默认方法?

答:

  • default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); }:返回一个顺序流,顺序流就是由一个线程执行操作。
  • default Stream<E> parallelStream() { return StreamSupport.stream(spliterator(), true); }:返回一个并行流,并行流背后可能有多个线程并行执行,使用并行流不需要显示管理线程。

2. 【笔试题】使用 Stream API 重写下面代码?

答:

  • Demo1

    // 传统代码
    List<Student> above90List = new ArrayList<> ();
    for(Student t : students) {
        if(t.getScore() > 90) {
            above90List.add(t);
        }
    }
    
    // 使用 Stream API
    List<Student> above90List = students.stream().filter(t -> t.getScore() > 90).collect(Collectors.toList());
    
  • Demo2

    // 传统代码
    List<String> nameList = new ArrayList<> (students.size());
    for(Student t : students) {
        nameList.add(t.getName());
    }
    
    // 使用 Stream API
    List<String> nameList = students.stream().map(Student::getName).collect(Collectors.toList());
    
  • Demo3

    // 传统代码
    List<String> nameList = new ArrayList<> ();
    for(Student t : students) {
        if(t.getScore() > 90) {
            nameList.add(t.getName());
        }
    }
    
    // 使用 Stream API
    List<String> above90Names = students.stream().filter(t -> t.getScore() > 90).map(Student::getName).collect(Collectors.toList());
    
  • 总结:

    // 代码直观易读,filter() 和 map() 都需要对流中的每个元素操作一次,一起使用并不会遍历两次,性能没有问题
    // 实际上,调用 filter() 和 map() 都不会执行任何实际的操作,它们只是在构建操作的流水线
    // 调用 collect() 才会触发实际的遍历执行,在一次遍历中完成过滤、转换以及收集结果的任务
    

3. 函数式编程中什么是中间操作、什么是终端操作?

答:

  • 中间操作:像 filter()map() 这种不实际触发执行、用于构建流水线、返回 Stream 的操作
  • 终端操作:像 collect() 这种触发实际执行、返回具体结果的操作

4. 函数式数据处理的概念和特点是什么?

答:

  • 概念:利用 Stream API 基本函数、声明式实现集合数据处理功能的组合式或者说链式的编程风格。
  • 特点:
    • 没有显示的循环迭代,循环过程被 Stream 的方法隐藏了。
    • 提供了声明式的处理函数,比如 filter,它封装了数据过滤的功能,而传统代码是命令式的,需要一步步的操作指令。
    • 流畅式接口,方法调用链接在一起,清晰易读。

5. Stream 流中的中间操作有哪些?

答:filtermapdistinctsortedskiplimitpeekmapToLongmapToIntmapToDoubleflatMap 等。

6. 【笔试题】使用 distinct 返回字符串列表中长度小于 3 的字符串、转换为小写、只保留唯一的?

答:

List<String> list = Arrays.asList(new String[] {"abc", "def", "hello", "Abc"});
List<String> retList = list.stream().filter(s -> s.length() <= 3).map(String::toLowerCase).distinct().collect(Collectors.toList());

7. 怎样理解 distinct 操作符?

答:

  • distinct 返回一个新的 Stream,过滤重复的元素,只留下唯一的元素,是否重复是根据 equals() 方法来比较的,可以与其他函数(比如 filtermap)结合使用
  • 虽然都是中间操作,但 distinctfiltermap 是不同的:
    • filtermap 都是无状态的,对于流中的每一个元素,处理都是独立的,处理后即交给流水线中的下一个操作。
    • distinct 是有状态的,在处理过程中,它需要在内部纪录之前出现过的元素,如果已经出现过,即重复元素,它就会过滤掉,不传递给流水线中的下一个操作。
  • 对于顺序流,内部实现时,distinct 操作会使用 HashSet 纪录出现过的元素;如果流是有顺序的,需要保留顺序,会使用 LinkedHashSet

8. 【笔试题】使用 sorted 过滤得到 90 以上的学生,然后按分数从高到低排序,分数一样的按名称排序?

答:

// 有两个 sorted() 方法:Stream<T> sorted() 和 Stream<T> sorted(Comparator<? super T> comparator)
// 它们都对流中的元素排序,都返回一个排序后的 Stream。第一个方法假定元素实现了 Comparable 接口;第二个方法接受一个自定义的 Comparator

List<Student> list = students.stream().filter(t -> t.getScore() > 90).sorted(Comparator.comparing(Student::getScore).reversed().thenComparing(Student::getName)).collect(Collectors.toList());

9. 【笔试题】使用 skip、limit 将学生列表按照分数排序,返回第 3 名到第 5 名?

答:

// 它们的定义:Stream<T> skip(long n) 和 Stream<T> limit(long maxSize)
// skip 跳过流中的 `n` 个元素,如果流中元素不足 `n` 个,返回一个空流,是有状态的中间操作
// limit 限制流的长度为 maxSize,是有状态的短路中间操作

List<Student> list = students.stream().sorted(Comparator.comparing(Student::getScore).reversed()).skip(2).limit(3).collect(Collectors.toList());

// skip 和 limit 只能根据元素数目进行操作,Java 9 增加了两个新方法,相当于更为通用的 skip 和 limit:
// default Stream<T> dropWhile(Predicate<? super T> predicate): 通用的 skip,在谓词返回为 true 的情况下一直进行 skip 操作,直到某次返回 false
// default Stream<T> takeWhile(Predicate<? super T> predicate):通用的 limit,在谓词返回为 true 的情况下一直接受,直到某次返回 false

10. 怎样理解 peek 操作符?

答:

  • peek 的定义为:Stream<T> peek(Consumer<? super T> action)
  • 含义:peek 返回的流与之前的流是一样的,没有变化,但它提供了一个 Consumer,会将流中的每一个元素传给该 Consumer这个方法的主要目的是支持调试,可以使用该方法观察在流水线中流转的元素
  • DemoList<String> above90Names = students.stream().filter(t -> t.getScore() > 90).peek(System.out::println).map(Student::getName).collect(Collectors.toList())

11. 怎样理解 mapToLong、mapToInt、mapToDouble 操作符?

答:

  • map 函数接受的参数是一个 Function<T, R>,为避免装修/拆箱,提高性能,Stream 还有如下返回基本类型特定的流的方法:

    • DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
    • IntStream mapToInt(ToIntFunction<? super T> mapper)
    • LongStream mapToLong(ToLongFunction<? super T> mapper)
  • DoubleStream``IntStream``LongStream 是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和:double sum = students.stream().mapToDouble(Student::getScore).sum()

12. 怎样理解 flatMap 操作符?

答:

  • flatMap 的定义为:<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
  • flatMap 接受一个函数 mapper,对流中的每一个元素,mapper 会将该元素转换为一个流 Stream,然后把新生成流的每一个元素传递给下一个操作。比如:

    List<String> lines = Arrays.asList(new String[] {"hello abc", "老马  编程"});
    List<String> words = lines.stream().flatMap(line -> Arrays.stream(line.split("\\s+"))).collect(Collectors.toList());
    System.out.println(words);
    
  • 这里的 mapper 将一行字符串按空白符分隔为了一个单词流,Arrays.stream 可以将一个数组转换为一个 stream 流,输出为:[hello, abc, 老马, 编程]

  • 实际上,flatMap 完成了一个 1n 的映射

13. Steam 流中的终端操作有哪些?

答:collectmaxmincountallMatchanyMatchnoneMatchfindFirstfindAnyforEachtoArrayreduce 等。

14. 怎么理解 max/mix?

答:

  • max/mix 的定义:

    • Optional<T> max(Comparator<? super T> comparator):返回流中的最大值。
    • Optional<T> min(Comparator<? super T> comparator):返回流中的最小值。
  • java.util.OptionalJava 8 引入的一个新类,它是一个泛型容器类,内部只有一个类型为 T 的单一变量 value,可能为 null,也可能不为 nullOptional 的作用是:用于准确地传递程序的语义,它清楚地表明,其代表的值可能为 null,程序员应该进行适当的处理

  • max/min 的例子中,通过声明返回值为 Optional,我们可以知道具体的返回值不一定存在,这发生在流中不含任何元素的情况下。
  • 举例,返回分数最高的学生(假定 students 不为空):Student student = students.stream().max(Comparator.comparing(Student::getScore).reversed()).get();

15. 怎么理解 count?

答:

  • 返回流中元素的个数
  • 举例,统计大于 90 分的学生个数:long above90Count = students.stream().filter(t -> t.getScore() > 90).count();

16. 怎么理解 allMatch、anyMatch、noneMatch?

答:

  • 这几个函数都接受一个谓词 Predicate,返回一个 boolean 值,用于判定流中元素是否满足一定的条件。它们的区别是:

    • allMatch:只有在流中所有元素都满足的条件下才返回 true
    • anyMatch:只要流中有一个元素满足条件就返回 true
    • noneMatch:只有流中所有元素都不满足条件才返回 true
  • 如果流为空,那么这几个函数的返回值都是 true

  • 都是短路操作,不一定需要处理所有元素就能得出结果。
  • 举例,判断是不是所有学生都及格了(不小于 60 分):boolean allPass = students.stream().allMatch(t -> t.getScore() >= 60);

17. 怎么理解 findFirst、findAny?

答:

  • 它们的定义为:

    • Optional<T> findFirst():返回第一个元素。
    • Optional<T> findAny():返回任一元素。
  • 它们的返回类型都是 Optional,如果流为空,返回 Optional.empty(),都是短路操作

  • 举例,随便找一个不及格的学生:Optional<Student> student = students.stream().filter(t -> t.getScore() < 60).findAny();

18. 怎么理解 forEach?

答:

  • void forEach(Consumer<? super T> action):在并行流中,不保证处理的顺序。
  • void forEachOrdered(Consumer<? super T> action):会保证按照流中元素的出现顺序进行处理。
  • 它们都接受一个 Consumer,对流中的每一个元素,传递元素给 Consumer
  • 举例,逐行打印大于 90 分的学生:students.stream().filter(t -> t.getScore() > 90).forEach(System.out::println);

19. 怎么理解 toArray?

答:

  • toArray 将流转换为数组:

    • Object[] toArray()
    • <A> A[] toArray(IntFunction<A[]> generetor)
  • IntFunction 的定义为:public interface IntFunction<R> { R apply(int value); }

  • 举例,获取 90 以上的学生数组:Student[] above90Arr = students.stream().filter(t -> t.getScore() > 90).toArray(Student[]::new);

20. 怎么理解 reduce?

答:

  • reduce 代表归约或者叫折叠,它是 maxmincount 的更为通用的函数,将流中的元素归约为一个值。有三个 reduce 函数:

    • 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)
  • 第二个 reduce 函数多了一个 identity 参数,表示初始值。

  • 第一个和第二个 reduce 函数的返回类型只能是流中元素的类型,而第三个 reduce 函数更为通用,它的归约类型可以自定义。另外,它多了一个 combiner 参数,combiner 用在并行流中,用于合并子线程的结果。
  • reduce 函数虽然更为通用,但比较费解,难以使用,一般情况下应该优先使用其他函数。collect 函数比 reduce 函数更为通用、强大和易用

21. 函数式编程中构建流的方式有哪些

答:

  • Collection 接口的 streamparallelStream 获取流

    • parallelStream 并行流内部会使用多线程,线程个数一般与系统的 CPU 核数一样,以充分利用 CPU 的计算能力。
    • 并行流内部会使用 Java 7 引入的 forkjoin 框架,即处理由 forkjoin 两个阶段组成:fork 就是将要处理的数据拆分为小块,多线程按小块进行并行计算; join 就是将小块的计算结果进行合并
    • 使用并行流,不需要任何线程管理的代码,就能实现并行。
  • Arrays 有一些 stream 方法,可以将数组或子数组转换为流

  • Stream 也有一些静态方法,可以构建流

22. 函数式数据处理思维是怎样的?

答:

  • 流定义了很多数据处理的基本函数,对于一个具体的数据处理问题,解决的主要思路就是组合利用这些基本函数,以声明式的方式简洁地实现期望的功能。这种思路就是函数式数据处理思维,相比直接利用容器类 API 的命令式思维,思考的层次更高。
  • Stream API 也与各种基于 Unix 系统的管道命令类似。
-------------本文结束感谢您的阅读-------------