函数式编程要点

一、介绍和入门

1、函数式编程介绍

函数式编程思想在于简化冗余代码,将函数式接口(接口中只有一个未实现的方法)作为最高抽象。它关注的是入参、返回、语法结构,是一种极简的编程方式。相对面向过程和面向对象编程,函数编程是面向数据处理和计算过程的抽象与组合

//在java中可以使用@FunctionalInterface标识函数式接口,其作用是检验接口中是否只有一个未实现的方法,如
@FunctionalInterface
public interface Function {
    R apply(T var1);
}

2、java中的函数式编程接口

在java.util.function包中,提供了大量规范的函数式接口,以避免过多使用自定义函数式接口

主要分为5类

  1. Consumer结尾:接收入参,没有返回
  2. Supplier结尾:没有入参,有返回
  3. Function结尾:接收入参,有返回
  4. Predicate结尾:断言,返回boolean类型参数
  5. Operator结尾:入参和返回参数类型相同
Consumer    接受一个参数,无返回值
BiConsumer    接受两个参数,无返回值
DoubleConsumer    接受一个double类型的参数,无返回值
IntConsumer    接受一个int类型的参数,无返回值
LongConsumer    接受一个long类型的参数,无返回值
ObjDoubleConsumer    接受一个自定义类型的参数和一个double类型的参数,无返回值
ObjIntConsumer    接受一个自定义类型的参数和一个int类型的参数,无返回值
ObjLongConsumer    接受一个自定义类型的参数和一个long类型的参数,无返回值
Function    接受一个参数,有返回值
BiFunction    接受两个参数,有返回值
DoubleFunction    接受一个double类型的参数,有返回值
IntFunction    接受一个int类型的参数,有返回值
LongFunction    接受一个long类型的参数,有返回值
IntToDoubleFunction    接受一个int类型的参数,返回一个double类型的值
IntToLongFunction    接受一个int类型的参数,返回一个long类型的值
LongToDoubleFunction    接受一个long类型的参数,返回一个double类型的值
LongToIntFunction    接受一个long类型的参数,返回一个int类型的值
DoubleToIntFunction    接受一个double类型的参数,返回一个int类型的值
DoubleToLongFunction    接受一个double类型的参数,返回一个long类型的值
ToDoubleBiFunction    接受两个参数,返回一个double类型的值
ToDoubleFunction    接受一个参数,返回一个double类型的值
ToIntBiFunction    接受两个参数,返回一个int类型的值
ToIntFunction    接受一个参数,返回一个int类型的值
ToLongBiFunction    接受两个参数,返回一个long类型的值
ToLongFunction    接受一个参数,返回一个long类型的值
BinaryOperator    接受两个相同类型的参数,返回一个相同类型的值
DoubleBinaryOperator    接受两个double类型的参数,返回一个double类型的值
DoubleUnaryOperator    接受一个double类型的参数,返回一个double类型的值
IntBinaryOperator    接受两个int类型的参数,返回一个int类型的值
IntUnaryOperator    接受一个int类型的参数,返回一个int类型的值
LongBinaryOperator    接受两个long类型的参数,返回一个long类型的值
LongUnaryOperator    接受一个long类型的参数,返回一个long类型的值
UnaryOperator    接受一个参数,返回一个相同类型的值
Predicate    接受一个参数,返回一个boolean类型的值
BiPredicate    接受两个参数,返回一个boolean类型的值
DoublePredicate    接受一个double类型的参数,返回一个boolean类型的值
IntPredicate    接受一个int类型的参数,返回一个boolean类型的值
LongPredicate    接受一个long类型的参数,返回一个boolean类型的值
Supplier    无参数,有返回值
BooleanSupplier    无参数,返回一个boolean类型的值
DoubleSupplier    无参数,返回一个double类型的值
IntSupplier    无参数,返回一个int类型的值
LongSupplier    无参数,返回一个long类型的值

 3、函数式编程配合Lambda表达式的简单实践

    public static void main(String[] args) {
        //匿名内部类
        Function function1 = new Function() {
            @Override
            public String apply(Integer i) {
                return String.valueOf(i);
            }
        };
        System.out.println(function1.apply(1));

        //简化
        Function function2 = i -> String.valueOf(i);
        System.out.println(function2.apply(1));

        //再简化
        Function function3 = String::valueOf;
        System.out.println(function3.apply(1));


        //多个参数
        BiFunction biFunction = (i, j) -> i + j;
        System.out.println(biFunction.apply(0, 1));


        //断言
        Predicate predicate = i -> i == 1;
        //判断是否满足条件
        System.out.println(predicate.test(2));
        //判断是否不满足条件
        System.out.println(predicate.negate().test(2));
    }

4、函数式编程特点

  1. 不可变性:在函数式编程中,数据是不可变的。这意味着一旦一个值被定义,它就不能被改变。这种不可变性有助于减少程序中的错误和副作用,因为你不必担心在某个地方意外地修改了数据。
  2. 无副作用:函数式编程中的函数通常没有副作用,即它们不会修改全局状态或产生其他外部影响。这使得函数更加可预测和可测试,因为你可以确信给定相同的输入,函数将始终产生相同的输出。
  3. 函数的纯粹性:在函数式编程中,函数是纯粹的,即它们的结果只取决于其输入参数,而不受外部状态的影响。这有助于简化代码和推理过程,因为你不必考虑函数之外的任何状态或变量。
  4. 高阶函数和闭包:函数式编程鼓励使用高阶函数(接受其他函数作为参数或返回函数的函数)和闭包(捕获其外部环境的函数)。这些特性使得函数更加灵活和可组合,可以方便地构建复杂的计算过程和数据结构。
  5. 抽象和组合:函数式编程强调通过抽象和组合来构建复杂的程序。你可以将计算过程分解为一系列简单的函数,然后将它们组合在一起以实现更复杂的功能。这种方式有助于提高代码的可读性、可维护性和可重用性

 5、函数式编程缺点

  1. 学习成本较高:函数式编程的思维方式与传统的命令式编程有很大的不同。它强调不可变性、无副作用和函数的纯粹性,这可能需要开发者花费更多的时间和精力来学习和适应。此外,函数式编程通常使用高阶函数和闭包等高级特性,这也增加了学习的难度。
  2. 性能问题:虽然函数式编程可以提高代码的可读性和可维护性,但在某些情况下,它可能不如命令式编程性能好。这主要是因为函数式编程强调不可变性和无副作用,这可能导致大量的数据复制和函数调用,从而增加了内存消耗和运行时间。然而,现代的函数式编程语言和编译器已经采用了许多优化技术来缓解这个问题。
  3. 调试困难:由于函数式编程强调函数的纯粹性和无副作用,这使得在调试过程中很难跟踪和定位错误。在命令式编程中,你可以通过设置断点、打印日志等方式来跟踪程序的执行过程。但在函数式编程中,由于函数之间没有共享的状态,因此很难确定错误发生的位置和原因。
  4. 与现有系统的兼容性:函数式编程与传统的命令式编程在思维方式和编程风格上有很大的不同。这可能导致在将函数式编程与现有的命令式系统进行集成时出现兼容性问题。例如,函数式编程强调不可变性和无副作用,这可能与现有系统中的某些特性和库不兼容。
  5. 可能的代码可读性问题:虽然函数式编程可以提高代码的简洁性和可维护性,但过度使用某些函数式编程特性(如无参风格、大量方法的组合)可能会影响代码的可读性。这使得其他开发者在阅读和理解代码时可能会遇到困难。

 二、进阶:函数响应式编程

1、Java8 Stream Api(流操作)

代码示例:

    public static void main(String[] args) {  
        // 创建一个整数列表  
        List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);  
  
        // 使用Stream API进行过滤、映射和聚合操作  
        List result = numbers.stream() // 创建流  
                .filter(n -> n % 2 == 0) // 过滤偶数  
                .map(n -> n * n) // 映射:将每个数平方  
                .collect(Collectors.toList()); // 聚合:将结果收集到一个新的列表中  
  
        // 输出结果[4, 16, 36, 64, 100]
        System.out.println(result);  
    } 

2、Stream Api特点

  1. 流水线操作:Stream API 的操作可以像流水线一样串联起来,形成一个大的操作链。每个操作都可以看作是对数据源的一个转换或处理步骤。
  2. 内部迭代:Stream API 采用了内部迭代的方式,具体的迭代过程则由流自行完成。这种方式简化了代码。                                               
  3. 不可变性:Stream API 中的流是不可变的。一旦一个流被创建,就不能再修改它的数据源或操作。每次对流进行修改或转换,都会返回一个新的流对象。这种不可变性保证了数据的安全性,避免了在并发环境下的数据竞争问题。
  4. 惰性求值:Stream API 采用了惰性求值的方式。这意味着在中间处理过程中,流只是对操作进行了记录,并不会立即执行。只有当执行到终止操作时,才会进行实际的计算。这种方式可以提高性能,避免不必要的计算。
  5. 丰富的操作:Stream Api简化了Java开发中数据运算、聚合、分组、转换、比较、过滤、遍历、排序等操作,通过使用Lambda表达式和函数式编程,可以简化冗余复杂的代码,这些操作可以方便地满足各种数据处理需求。

3、Stream Api使用注意点

  1. 流的操作默认多线程并行执行,对于简单操作其性能可能不如传统写法,此外在并发编程中流处理的中间操作过程中不应该介入有状态数据,或将流中间操作的结果输出。
  2. 流操作并不是每次对一个中间过程进行处理,而是每次走完整个处理链。
  3. 相对传统写法经常要保存中间结果,流操作的好处在未输出结果前,并不会占用内存空间。

 4、Stream Api的中间操作和终端操作

  1. 中间操作
    中间操作会返回一个新的流,并允许链式操作。这些操作不会立即执行,而是惰性求值的,即只有当终端操作被触发时,中间操作才会被应用到数据源上。常见的中间操作有:

    • filter(Predicate predicate)
      • 过滤流中的元素,只保留满足给定谓词的元素。
    • map(Function mapper)
      • 将流中的每个元素映射成一个新的元素,通常用于转换数据类型或提取信息。
    • flatMap(Function> mapper)
      • 将流中的每个元素映射成一个新的流,然后将这些流合并成一个流。这通常用于将多个流合并成一个,或将嵌套的数据结构展平。
    • limit(long maxSize)
      • 截断流,使其只包含前 maxSize 个元素。这是一种短路操作,即一旦达到指定的大小,就会停止处理更多的元素。
    • skip(long n)
      • 跳过流中的前 n 个元素。如果流中的元素不足 n 个,则返回一个空流。
    • distinct()
      • 返回一个包含所有不同元素的新流。这是通过流的元素的 equals() 方法进行比较的。
    • sorted() 或 sorted(Comparator comparator)
      • 返回一个新流,其中的元素按自然顺序排序,或者根据提供的 Comparator 进行排序。
    • peek(Consumer action)
      • 对流中的每个元素执行给定的操作,并返回一个新的流包含原始元素。这主要用于调试目的,因为它允许你查看流中的元素而不改变它们。
    • mapToInt(ToIntFunction mapper)mapToLong(ToLongFunction mapper)mapToDouble(ToDoubleFunction mapper)
      • 这些操作将流中的元素映射成基本数据类型(int, long, double)的流,如 IntStreamLongStreamDoubleStream。这对于需要进行数值计算的情况非常有用。
  2. 终端操作
    终端操作会触发实际的计算,并关闭流。流只能被遍历一次,因此终端操作之后,流就不能再被使用了。常见的终端操作有:

    • forEach(Consumer action)
      • 对流中的每个元素执行给定的操作。这通常用于遍历流并执行某种副作用。
    • collect(Collector collector)
      • 使用提供的Collector对流中的元素进行归约操作,并将结果收集到一个目标对象中,通常是集合或其他数据结构。
    • toArray()
      • 将流中的元素收集到一个数组中。这个方法有两种形式:无参的toArray(),它返回一个Object[],以及toArray(IntFunction generator),它允许你指定数组的类型和大小。
    • reduce(T identity, BinaryOperator accumulator)
      • 使用给定的累积函数对流中的元素进行归约操作,并返回归约的结果。identity是归约的初始值。
    • min(Comparator comparator) 和 max(Comparator comparator)
      • 根据提供的比较器返回流中的最小或最大元素。返回的是一个Optional,表示最小或最大元素可能存在也可能不存在。
    • count()
      • 返回流中的元素数量。这是一个长整型值,可以处理比Integer.MAX_VALUE更大的流。
    • anyMatch(Predicate predicate)
      • 判断流中是否有任何元素匹配给定的谓词。如果有一个元素满足条件,就返回true
    • allMatch(Predicate predicate)
      • 判断流中的所有元素是否都匹配给定的谓词。只有当所有元素都满足条件时,才返回true
    • noneMatch(Predicate predicate)
      • 判断流中是否没有任何元素匹配给定的谓词。如果所有元素都不满足条件,就返回true
    • findFirst()
      • 返回流中的第一个元素。返回的是一个Optional,表示第一个元素可能存在也可能不存在。
    • findAny()
      • 返回流中的任意一个元素。在并行流中,这个方法可能会更高效,因为它允许从任何部分获取元素。返回的也是一个Optional

这两种操作类型一起构成了 Stream API 强大的数据处理和分析能力。中间操作允许我们构建复杂的查询和转换管道,而终端操作则负责触发实际的计算和结果提取。这些操作在对应api注释中有介绍。

函数式编程要点_第1张图片

5、关于Stream Api实现原理

Stream Api涵盖了函数式编程和响应式编程的思想,其响应式编程思想主要体现在流水线操作、异步编程。对于响应式编程可以通过Stream Api源码或者网络资源学习。 

核心组件

  1. Stream 接口:这是Stream API的入口点,它定义了许多操作,如filtermapreduce等。这些操作可以分为两类:中间操作(返回一个新的Stream以允许链式调用)和终止操作(返回一个非Stream的结果或者产生副作用)。
  2. Pipeline 概念:当你对一个Stream调用一系列方法时,你实际上是在构建一个处理管道。这个管道是惰性的,意味着只有在终止操作被调用时才会执行。
  3. Spliterator 接口:这是Stream API用来遍历和处理数据的关键组件。它结合了“split”(分解)和“iterator”(迭代器)的概念,允许数据被有效地并行处理。
  4. 内部类:Stream接口有许多内部类实现,如ReferencePipelineIntPipelineLongPipelineDoublePipeline等,这些类为不同类型的数据提供了特定的实现。

源码包含以下几个部分

  1. 创建Stream:当你从一个集合调用.stream()方法时,通常会返回一个对该集合特定实现的Stream。例如,ArrayList会有一个返回ArrayListSpliterator的方法。
  2. 中间操作:当你调用如filtermap这样的中间操作时,Stream API通常会在当前的Stream上添加一个新的操作节点,而不是立即执行它。这些操作被组合成一个链式结构。
  3. 终止操作:当你调用一个终止操作时,如collectforEach,Stream API会开始遍历Spliterator,并执行之前定义的所有中间操作。这个过程可能是顺序的,也可能是并行的,取决于Stream的创建方式。
  4. 优化和短路:Stream API在内部进行了许多优化,以确保操作尽可能地高效。例如,它可能会合并连续的filter操作,或者在遇到短路操作(如anyMatch)时提前终止遍历。
  5. 错误处理:如果在处理过程中发生错误(如传递给map的函数抛出异常),Stream API通常会立即停止执行并传播这个异常。
  6. 并行处理:对于并行Stream,数据会被分成多个部分,每个部分都由一个线程处理。这需要额外的同步和协调,以确保结果的正确性。

本文引用

函数式编程有哪些好处?_函数式编程语言有哪些_Js函数式编程好处 - 腾讯云开发者社区 - 腾讯云

Stream流API总结_stream流常用api-CSDN博客

 java Stream类的 超全API及实战总结_java stream api-CSDN博客

在Stream流中添加中间操作_stream添加元素-CSDN博客

在Stream上添加终端操作_tounmodifiableset-CSDN博客

你可能感兴趣的:(java,程序人生,经验分享,后端)