在 Java 8 之前,Java 是没有很明确的函数式编程这么一说的,那之前的 Java 代码都是类、方法等组成的,若想要实现一个很简单的功能往往要写上很多代码,这就非常地不方便。于是,在 Java 8 中更新了一系列与函数式编程相关的内容,比如 Lambda 表达式(新增)、函数式接口(强化),以及配合函数式接口使用的 Stream 流(新增)等。而 Java 中函数式编程的典型代表就是 Lambda 表达式了。
顾名思义,函数式编程就是以函数为主要的“对象”,通过函数的方式来进行编程,与之相对应的就是类和对象。编程方式中也常常分为两种类型,一种是面向过程编程,另外一种就是面向对象编程。而函数式编程显而易见就是偏向于面向过程编程了。但是 Java 本身实际没有真正的函数,类中的“函数”称为方法更为合适,因此 Java 中的函数式编程的写法也和类方法的写法有相似之处,但是其调用方式却是以函数的方式。
函数式编程设计初衷就是简化代码用的,比如一个很小的,只有一个语句的“函数”,为了实现它却要大费周章地写一个方法给他,导致代码十分的冗长,这就很不合适,而使用函数式编程,不仅仅可以省去这些冗长的代码,还可以使代码的逻辑更加清晰。Java 函数式编程最核心的内容就是 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 表达式,可以:
下面我们来着重讲解 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 流的使用。
函数式接口本质上还是接口,只不过它比较特殊,它有且仅有一个抽象方法。Java 中内置了许多函数式接口,典型的四种是供给型接口、消费型接口、断言型接口和函数型接口。下面是一个简单的自定义函数式接口:
@FunctionalInterface // 函数式接口的注解,并非必须的
interface Square {
int caculate(int a);
}
函数式接口的使用方式一般有两种,使用 Lambda 表达式或者使用方法引用。
前文已经详述,此处不再赘述。
这里以 System.out.println 方法为例进行说明。当我们将其写成下面这种形式的时候,表示方法调用:
System.out.println()
但我们知道,Lambda 表达式是不会直接调用的,而是作为一种形式,告诉函数式接口,要怎么做。因此,我们只需要一个该方法的引用即可,于是就有了方法引用,其写法就是将方法名前面的点号改成双冒号:
System.out::println
上述写法就表示引用 System.out 类中的 println 这个方法,而不是调用它。
注意:下述出现的源代码皆为 Oracle JDK 21 中的,如有不同,请检查 JDK 版本。
供给型接口的源代码是这样的:
@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 表达式只会在调用它们的时候才会加载。就是提前加载和运行时加载的区别,当然,还可以简化代码。
下面是消费型接口的源代码(省去注释了):
@FunctionalInterface
public interface Consumer {
void accept(T t);
default Consumer andThen(Consumer super T> 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
}
}
下面是断言型接口的源代码(省去注释了):
@FunctionalInterface
public interface Predicate {
boolean test(T t);
default Predicate and(Predicate super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate negate() {
return (t) -> !test(t);
}
default Predicate or(Predicate super T> 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 super T> 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 方法,这里不再赘述,使用方式和上述类似。
函数型接口就是函数型接口,它只是函数式接口中的一种,不能搞混了。下面是函数型接口的源代码(省去注释了):
@FunctionalInterface
public interface Function {
R apply(T t);
default Function compose(Function super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default Function andThen(Function super R, ? extends V> 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(方法参数)在前执行。
虽然还有其他的函数式接口,但是它们都没有上述四种那么常用,或者它们只是在某一情形下才会被使用,不像上面的四种被广泛使用,这里就不在对它们展开讲述了。