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 表达式:

@FunctionalInterface
interface Formula { // 函数式接口(后文会讲)
    double calculate(double x);
}

public class Test {
    public static void main(String[] args) {
        Formula f = x -> 3 * x * x + 2 * x + 1; // Lambda 表达式
        
        double result = f.calculate(2.5);
        System.out.println(result); // Output: 24.75
    }
}

C++ Lambda 表达式:

#include 

int main() {
    auto f = [](double x) -> double {return 3 * x * x + 2 * x + 1; };  // Lambda 表达式

    double result = f(2.5);
    std::cout << result << std::endl;  // Output: 24.75
    return 0;
}

Python Lambda 表达式:

f = lambda x: 3 * x**2 + 2 * x + 1  # Lambda 表达式

result = f(2.5)
print(result)  # Output: 24.75

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

Java:

public class Test {
    public static void main(String[] args) {
        double result = f(2.5);
        System.out.println(result); // Output: 24.75
    }

    static double f(double x) { // 定义方法
        return 3 * x * x + 2 * x + 1;
    }
}

C++:

#include 

double f(double x) {  // 定义函数
    return 3 * x * x + 2 * x + 1;
}

int main() {
    double result = f(2.5);
    std::cout << result << std::endl;  // Output: 24.75
    return 0;
}

Python:

def f(x: float) -> float:  # 定义函数
    return 3 * x**2 + 2 * x + 1

result = f(2.5)
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 中内置了许多函数式接口,典型的四种是供给型接口、消费型接口、断言型接口和函数型接口。下面是一个简单的自定义函数式接口:

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

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)接口

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

@FunctionalInterface
public interface Supplier {

    T get();
}

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

import java.util.function.Supplier;

public class Test {
    public static String getData(Supplier supplier){
        return supplier.get(); // 返回得到(get)的数据
    }

    public static void main(String[] args) {
        String string = getData(() -> "Java");
        System.out.println(string); // Output: Java
    }
}

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

4.2 消费型(Consumer)接口

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

@FunctionalInterface
public interface Consumer {

    void accept(T t);

    default Consumer andThen(Consumer after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

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

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

import java.util.function.Consumer;

public class Test {
    public static void process(String data, Consumer consumer){
        consumer.accept(data); // 抽象方法收集(accept)数据(data)
    }

    public static void main(String[] args) {
        // 用 System.out.println 方法对数据“Java”进行处理
        process("Java", System.out::println); // Output: Java
        // 用 Lambda 表达式对数据“Java”进行处理
        process("Java", (String data) -> System.out.println(data.toUpperCase())); // Output: JAVA
    }
}

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

import java.util.function.Consumer;

public class Test {
    public static void process(String data, Consumer consumer_1, Consumer consumer_2){
        consumer_1.andThen(consumer_2).accept(data);
        // 与前面的代码类似,但这里是先让 consumer_1 处理,再让 consumer_2 处理
        // 其实吧,和把两个分开写区别不大,但是可以简化代码
    }

    public static void main(String[] args) {
        process("Java",
                (String data) -> System.out.println(data.toLowerCase()), // Output: java
                (String data) -> System.out.println(data.toUpperCase())); // Output: JAVA
    }
}

4.3 断言型(Predicate)接口

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

@FunctionalInterface
public interface Predicate {

    boolean test(T t);

    default Predicate and(Predicate other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate negate() {
        return (t) -> !test(t);
    }

    default Predicate or(Predicate other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static  Predicate isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

    @SuppressWarnings("unchecked")
    static  Predicate not(Predicate target) {
        Objects.requireNonNull(target);
        return (Predicate)target.negate();
    }
}

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

import java.util.function.Predicate;

public class Test {
    public static boolean predicateTest(Integer integer, Predicate predicate) {
        return predicate.test(integer); // 测试 integer 数据并返回测试结果(true 或 false)
    }

    public static void main(String[] args) {
        boolean bool = predicateTest(4, (Integer integer) -> integer > 6); // 判断输入的数据是否大于 6
        System.out.println(bool); // Output: false
    }
}

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

public class Test {
    public static boolean predicateTest(Integer integer, Predicate predicate_1, Predicate predicate_2) {
        return predicate_1.and(predicate_2).test(integer);
        // 让两个测试器(Predicate)分别对数据测试,结果取“与”运算
    }

    public static void main(String[] args) {
        boolean bool = predicateTest(4,
                (Integer integer) -> integer > 3,
                (Integer integer) -> integer < 6); // 判断输入的数据是否大于 3 且小于 6
        System.out.println(bool); // Output: true
    }
}

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

4.4 函数型(Function)接口

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

@FunctionalInterface
public interface Function {

    R apply(T t);

    default  Function compose(Function before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default  Function andThen(Function after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static  Function identity() {
        return t -> t;
    }
}

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

import java.util.function.Function;

public class Test {
    public static Integer change(String string, Function function) {
        return function.apply(string); // 返回转换后的数据
    }

    public static void main(String[] args) {
        Integer integer = change("Java", (String string) -> string.length()); // 转换得到 string 的长度
        System.out.println(integer); // Output: 4
    }
}

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

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

你可能感兴趣的:(编程秘籍:Java,Java,函数式编程,Lambda,表达式,函数式接口)