• Java 函数式编程


    一、简介

    1.1 函数式编程的引进

    Java 8 之前,Java 是没有很明确的函数式编程这么一说的,那之前的 Java 代码都是类、方法等组成的,若想要实现一个很简单的功能往往要写上很多代码,这就非常地不方便。于是,在 Java 8 中更新了一系列与函数式编程相关的内容,比如 Lambda 表达式(新增)、函数式接口(强化),以及配合函数式接口使用的 Stream 流(新增)等。而 Java 中函数式编程的典型代表就是 Lambda 表达式了。

    1.2 函数式编程

    顾名思义,函数式编程就是以函数为主要的“对象”,通过函数的方式来进行编程,与之相对应的就是类和对象。编程方式中也常常分为两种类型,一种是面向过程编程,另外一种就是面向对象编程。而函数式编程显而易见就是偏向于面向过程编程了。但是 Java 本身实际没有真正的函数,类中的“函数”称为方法更为合适,因此 Java 中的函数式编程的写法也和类方法的写法有相似之处,但是其调用方式却是以函数的方式。

    函数式编程设计初衷就是简化代码用的,比如一个很小的,只有一个语句的“函数”,为了实现它却要大费周章地写一个方法给他,导致代码十分的冗长,这就很不合适,而使用函数式编程,不仅仅可以省去这些冗长的代码,还可以使代码的逻辑更加清晰。Java 函数式编程最核心的内容就是 Lambda 表达式与函数式接口。

    二、Lambda 表达式

    2.1 为什么用 Lambda 表达式

    Lambda 表达式,又叫匿名表达式,不仅仅在 Java 中有它,在其他的编程语言中也有它,大多功能相近,都是为了简化语法。一般来说,数学上的简单函数都会用它来实现,因为大部分的数学表达式都是很简单的一个公式,说白了,在编程中就是一条语句罢了,因此为了省去冗长的代码,就会 Lambda 表达式来表示这些公式,这样也更加符合数学表达式的样子。我们来看几个例子就明白了:

    数学表达式:

    Java Lambda 表达式:

    1. @FunctionalInterface
    2. interface Formula { // 函数式接口(后文会讲)
    3. double calculate(double x);
    4. }
    5. public class Test {
    6. public static void main(String[] args) {
    7. Formula f = x -> 3 * x * x + 2 * x + 1; // Lambda 表达式
    8. double result = f.calculate(2.5);
    9. System.out.println(result); // Output: 24.75
    10. }
    11. }

    C++ Lambda 表达式:

    1. #include
    2. int main() {
    3. auto f = [](double x) -> double {return 3 * x * x + 2 * x + 1; }; // Lambda 表达式
    4. double result = f(2.5);
    5. std::cout << result << std::endl; // Output: 24.75
    6. return 0;
    7. }

    Python Lambda 表达式:

    1. f = lambda x: 3 * x**2 + 2 * x + 1 # Lambda 表达式
    2. result = f(2.5)
    3. print(result) # Output: 24.75

    不难看出,这几个语言的 Lmabda 表达式都如出一辙的简洁!下面是不用 Lambda 表达式的实现方式:

    Java:

    1. public class Test {
    2. public static void main(String[] args) {
    3. double result = f(2.5);
    4. System.out.println(result); // Output: 24.75
    5. }
    6. static double f(double x) { // 定义方法
    7. return 3 * x * x + 2 * x + 1;
    8. }
    9. }

    C++:

    1. #include
    2. double f(double x) { // 定义函数
    3. return 3 * x * x + 2 * x + 1;
    4. }
    5. int main() {
    6. double result = f(2.5);
    7. std::cout << result << std::endl; // Output: 24.75
    8. return 0;
    9. }

    Python:

    1. def f(x: float) -> float: # 定义函数
    2. return 3 * x**2 + 2 * x + 1
    3. result = f(2.5)
    4. print(result) # Output: 24.75

    不难看出,Lambda 表达式的优势在哪里了吧?(虽然看起来用了好像还是比较复杂)从形式上来看,Python 和数学表达式最像,其次是 Java,然后是 C++;从结构变化上来看,C++ 变化最小,几乎和一般的函数一样,然后是 Python,稍微有一点变化,而 Java 的 Lambda 表达式与一般“函数”相比,差异较大。

    总的来说,使用 Lambda 表达式,可以:

    • 让代码中的数学表达式更清晰;
    • 调用 Lambda 表达式如同书写数学公式一般简单;
    • 让代码更加紧凑,简洁;

    下面我们来着重讲解 Java 的 Lambda 表达式。

    2.2 Java Lambda 表达式

    Java 中 Lambda 表达式的一般形式是这样的:

    parameter -> expression

    或者下面这样的(表达式主体部分更多了):

    (parameter_1, parameter_2, ...) -> {expression_1; expression_2; ...}

    前面括号(只有一个参数时可以不写括号)里面是参数列表,其类型可以不用显式声明,编译器可以识别,大括号里面是 Lambda 表达式的主体语句,当只有一条语句时,可以不写大括号,且无需指定返回值,但是使用大括号了则需要用 return 指定返回值(不然编译器返回哪一条语句的值,就算只有一条语句)。

    Python 和 C++ 中的 Lambda 表达式是可以直接调用的,但 Java 中的 Lambda 表达式一定要一个函数式接口配合使用才行,关于函数式接口,后文会详解。

    个人感觉,Java 在函数式编程这一块还有待加强,Lambda 表达式无法直接编写调用,必须配合函数式接口才行,这不就相当于用函数式接口替代原来的方法了吗?只不过后续内置了很多函数式接口,才使得 Lambda 表达式在某些地方可以“直接使用”(实际并非直接,还是要配合函数式接口,只不过这个接口不用自己写),最典型的地方就是 Stream 流的使用。

    三、函数式接口

    3.1 函数式接口简介

    函数式接口本质上还是接口,只不过它比较特殊,它有且仅有一个抽象方法。Java 中内置了许多函数式接口,典型的四种是供给型接口、消费型接口、断言型接口和函数型接口。下面是一个简单的自定义函数式接口:

    1. @FunctionalInterface // 函数式接口的注解,并非必须的
    2. interface Square {
    3. int caculate(int a);
    4. }

    3.2 函数式接口的使用方式

    函数式接口的使用方式一般有两种,使用 Lambda 表达式或者使用方法引用。

    3.2.1 Lambda 表达式

    前文已经详述,此处不再赘述。

    3.2.2 方法引用

    这里以 System.out.println 方法为例进行说明。当我们将其写成下面这种形式的时候,表示方法调用:

    System.out.println()

    但我们知道,Lambda 表达式是不会直接调用的,而是作为一种形式,告诉函数式接口,要怎么做。因此,我们只需要一个该方法的引用即可,于是就有了方法引用,其写法就是将方法名前面的点号改成双冒号:

    System.out::println

    上述写法就表示引用 System.out 类中的 println 这个方法,而不是调用它。

    四、四种典型的内置函数式接口

    注意:下述出现的源代码皆为 Oracle JDK 21 中的,如有不同,请检查 JDK 版本。

    4.1 供给型(Supplier)接口

    供给型接口的源代码是这样的:

    1. @FunctionalInterface
    2. public interface Supplier {
    3. T get();
    4. }

    它只有一个没有参数的抽象方法 get,返回类型为泛型(啥都行)。由于它不接受参数,只返回值,因此被称为供给型接口,也被称为生产型接口。下面是一个使用例子:

    1. import java.util.function.Supplier;
    2. public class Test {
    3. public static String getData(Supplier supplier){
    4. return supplier.get(); // 返回得到(get)的数据
    5. }
    6. public static void main(String[] args) {
    7. String string = getData(() -> "Java");
    8. System.out.println(string); // Output: Java
    9. }
    10. }

    或许有人会问,这样做有什么用呢?它与直接写方法调用不同的地方在于,用方法调用会提前加载,而用函数式接口和 Lambda 表达式只会在调用它们的时候才会加载。就是提前加载和运行时加载的区别,当然,还可以简化代码。

    4.2 消费型(Consumer)接口

    下面是消费型接口的源代码(省去注释了):

    1. @FunctionalInterface
    2. public interface Consumer {
    3. void accept(T t);
    4. default Consumer andThen(Consumersuper T> after) {
    5. Objects.requireNonNull(after);
    6. return (T t) -> { accept(t); after.accept(t); };
    7. }
    8. }

    它有一个泛型参数 t 的抽象方法 accept,不返回任何值,因此被称为消费型接口。除此之外,它还有一个默认方法 andThen,接受一个名为 after 的消费型接口,返回消费型接口。从参数名称和方法名称上可以知道,andThen 表示消费型接口接受到(accept)数据之后该怎么办,而之后(after)处理它的另外一个消费型接口就是 after,它是来对接受到的数据进行”消费“的,也就是对数据进行处理。

    综上所述,我们可以将其理解为一个对数据的处理器。下面是一个示例代码:

    1. import java.util.function.Consumer;
    2. public class Test {
    3. public static void process(String data, Consumer consumer){
    4. consumer.accept(data); // 抽象方法收集(accept)数据(data)
    5. }
    6. public static void main(String[] args) {
    7. // 用 System.out.println 方法对数据“Java”进行处理
    8. process("Java", System.out::println); // Output: Java
    9. // 用 Lambda 表达式对数据“Java”进行处理
    10. process("Java", (String data) -> System.out.println(data.toUpperCase())); // Output: JAVA
    11. }
    12. }

    至于 andThen 方法,就相当于一个附加的功能,可以让多个处理器(或者消费型接口)对同一数据按顺序进行处理,如下例:

    1. import java.util.function.Consumer;
    2. public class Test {
    3. public static void process(String data, Consumer consumer_1, Consumer consumer_2){
    4. consumer_1.andThen(consumer_2).accept(data);
    5. // 与前面的代码类似,但这里是先让 consumer_1 处理,再让 consumer_2 处理
    6. // 其实吧,和把两个分开写区别不大,但是可以简化代码
    7. }
    8. public static void main(String[] args) {
    9. process("Java",
    10. (String data) -> System.out.println(data.toLowerCase()), // Output: java
    11. (String data) -> System.out.println(data.toUpperCase())); // Output: JAVA
    12. }
    13. }

    4.3 断言型(Predicate)接口

    下面是断言型接口的源代码(省去注释了):

    1. @FunctionalInterface
    2. public interface Predicate {
    3. boolean test(T t);
    4. default Predicate and(Predicatesuper T> other) {
    5. Objects.requireNonNull(other);
    6. return (t) -> test(t) && other.test(t);
    7. }
    8. default Predicate negate() {
    9. return (t) -> !test(t);
    10. }
    11. default Predicate or(Predicatesuper T> other) {
    12. Objects.requireNonNull(other);
    13. return (t) -> test(t) || other.test(t);
    14. }
    15. static Predicate isEqual(Object targetRef) {
    16. return (null == targetRef)
    17. ? Objects::isNull
    18. : object -> targetRef.equals(object);
    19. }
    20. @SuppressWarnings("unchecked")
    21. static Predicate not(Predicatesuper T> target) {
    22. Objects.requireNonNull(target);
    23. return (Predicate)target.negate();
    24. }
    25. }

    可以看到啊,断言型接口的源代码相比于前面两个那可复杂多了,但其实它使用起来也是很简单的。从其名字中可以知道,断言型接口是用来判断 true 和 false 的,它有一个泛型参数为 t 的抽象方法 test,返回布尔值。下面是一个简单的示例:

    1. import java.util.function.Predicate;
    2. public class Test {
    3. public static boolean predicateTest(Integer integer, Predicate predicate) {
    4. return predicate.test(integer); // 测试 integer 数据并返回测试结果(true 或 false)
    5. }
    6. public static void main(String[] args) {
    7. boolean bool = predicateTest(4, (Integer integer) -> integer > 6); // 判断输入的数据是否大于 6
    8. System.out.println(bool); // Output: false
    9. }
    10. }

    断言型接口的其他方法从名字上来看,and、or、negate 就是与、或、非,它们可以将这些逻辑运算附加到测试(test 方法)结果上,使用方式和之前消费型接口的 andThen 非常像。

    1. public class Test {
    2. public static boolean predicateTest(Integer integer, Predicate predicate_1, Predicate predicate_2) {
    3. return predicate_1.and(predicate_2).test(integer);
    4. // 让两个测试器(Predicate)分别对数据测试,结果取“与”运算
    5. }
    6. public static void main(String[] args) {
    7. boolean bool = predicateTest(4,
    8. (Integer integer) -> integer > 3,
    9. (Integer integer) -> integer < 6); // 判断输入的数据是否大于 3 且小于 6
    10. System.out.println(bool); // Output: true
    11. }
    12. }

    至于 or 和 negate 方法,这里不再赘述,使用方式和上述类似。

    4.4 函数型(Function)接口

    函数型接口就是函数型接口,它只是函数式接口中的一种,不能搞混了。下面是函数型接口的源代码(省去注释了):

    1. @FunctionalInterface
    2. public interface Function {
    3. R apply(T t);
    4. default Function compose(Functionsuper V, ? extends T> before) {
    5. Objects.requireNonNull(before);
    6. return (V v) -> apply(before.apply(v));
    7. }
    8. default Function andThen(Functionsuper R, ? extends V> after) {
    9. Objects.requireNonNull(after);
    10. return (T t) -> after.apply(apply(t));
    11. }
    12. static Function identity() {
    13. return t -> t;
    14. }
    15. }

    函数型接口从它的名称上可能还无法知道它是干什么的,但其实说白了,它相当于一个转换器。它有一个泛型参数 t 的抽象方法 apply,可以通过泛型参数来返回一个新的数据,这个数据类型与参数可以不一样,但它和参数有一定的函数关系,这就是函数型接口名称的由来。下面是一个简单的示例:

    1. import java.util.function.Function;
    2. public class Test {
    3. public static Integer change(String string, Function function) {
    4. return function.apply(string); // 返回转换后的数据
    5. }
    6. public static void main(String[] args) {
    7. Integer integer = change("Java", (String string) -> string.length()); // 转换得到 string 的长度
    8. System.out.println(integer); // Output: 4
    9. }
    10. }

    关于其默认方法 andThen,和消费型接口 andThen 方法的左右相同,此处不再赘述。而 compose 方法就比较有意思了,从它的源代码来看,它可以说是 andThen 方法的对立面,即它的执行顺序与 andThen 相比是相反的。andThen 是 after(方法参数)在后执行,而 compose 则是 before(方法参数)在前执行。

    虽然还有其他的函数式接口,但是它们都没有上述四种那么常用,或者它们只是在某一情形下才会被使用,不像上面的四种被广泛使用,这里就不在对它们展开讲述了。

  • 相关阅读:
    【系统设计】邻近服务
    vue v-model
    Nexus-3.41.1安装
    SpringCloud 07 Ribbon实现负载均衡
    百度智能云千帆大模型平台再升级,SDK版本开源发布!
    什么是以太坊域名服务(ENS)?
    [附源码]Python计算机毕业设计出版社样书申请管理系统
    Rust中的输入输出格式变化(非常详细)
    1-乙基-3-甲基咪唑醋酸盐([EMIM][Ac]);甲基三辛基醋酸铵[N(1,8,8,8)][Ac]齐岳离子液体
    代码随想录算法训练营Day25|leetcode216 组合总和III,leetcode17 电话号码的字母组合
  • 原文地址:https://blog.csdn.net/weixin_62651706/article/details/133965723