Java基础 - Java函数式编程 - 函数式接口
Introduction
函数式编程是一种编程范式,与其相对的是面向对象编程。
- OOP:将数据和操作该数据的行为(方法)封装在对象中。程序的设计围绕着对象及其相互作用展开。
- FP:它将计算视为数学函数的求值过程。程序的设计是通过组合一系列函数来完成的。
不像JavaScript,函数可以被当作参数传入函数中。在 Java 中,函数无法独立于类或对象存在。那么如何让Java代码可以传递函数作为参数呢?答案是函数式接口。
回调函数
考虑以下场景:
文件下载是一个耗时的任务,我希望调用文件下载函数(downloadFile)之后立刻返回,继续处理别的数据,当文件下载结束之后,downloadFile函数可以自动调用一个onSuccess函数来对下载的文件作处理。
如果同步的对下载文件作处理:
1
2
3
4
5
6
7
8
9
10public File downloadFileSync(String url) {
// ... 执行耗时的下载逻辑 ...
File file = new File("...");
return file;
}
// 调用代码
System.out.println("开始下载...");
File myFile = downloadFileSync("http://example.com/largefile.zip"); // <--- 程序会在这里卡住10秒钟
System.out.println("下载完成!文件路径:" + myFile.getPath()); // 10秒后才会执行这句在 downloadFileSync 方法完成并返回结果之前,调用它的线程会被阻塞(Block),无法执行任何其他操作。
我们可以考虑使用两个线程来解决这个问题:一个是主线程,另一个线程负责执行下载以及之后的回调。思路:将函数作为参数传递到downloadFile的函数中。但是Java不支持函数作为参数,有什么解决方法呢?答案是:将函数包装在一个对象里。
1 |
|
1 |
|
1 |
|
函数式接口
Java 8对上述模式中的“接口”进行了一次概念上的升级。如果一个接口只有一个抽象方法,它就被称为函数式接口。一个函数式接口是指有且仅有一个抽象方法的接口。Java 提供了 @FunctionalInterface 注解来在编译时强制校验这一点。一个经典的例子是 java.lang.Runnable:
1 |
|
创建一个线程的本质可以理解为启动一个线程来执行某个函数,这个函数通过Runnable包装起来,使得这个函数可以被当作参数传递给其他函数。
直接实现 Runnable 接口
1 |
|
使用匿名类实现 Runnable
1 |
|
匿名类是 Java 中的一种特殊类,用来创建一个没有名字的类并立即实例化它。本质上,以上代码创建了一个匿名类,它实现了 Runnable 接口,并提供了 run() 方法的具体实现。
使用 Lambda 表达式
1 |
|
Lambda函数是前两个实现方法的语法糖。它允许你直接提供那个唯一抽象方法的实现,编译器会自动为你完成“创建对象”这个包装过程。
java.util.function
java.util.function 包提供一套通用、标准化的函数式接口。
1 |
|
有了 java.util.function,你不再需要定义 StringOperation,可以直接使用 Function<String, String>
。StringOperation这个函数式接口本质上定义了一个“接受一个 String,返回一个 String”的函数载体。Function<String, String>
精确地描述了“接受一个 String,返回一个 String”这个函数签名。
Function<String, String>
是预先定义好的接口,它就是一个函数式接口,只不过提供了统一的规范:
1 |
|
Function<T, R>
- 接口方法:
R apply(T t)
- 接受一个类型为 T 的参数,经过转换后,返回一个类型为 R 的结果。这是最典型的“输入 -> 输出”模型。
1 |
|
Predicate
- 接口方法:
boolean test(T t)
- 接受一个类型为 T 的参数,返回一个布尔值。用于判断输入是否满足某个条件。
1 |
|
Consumer
- 接口方法:
void accept(T t)
- 接受一个类型为 T 的参数,执行某个操作,但没有返回值。
1 |
|
Supplier
- 接口方法:
T get()
- 不接受任何参数,但返回一个类型为 T 的结果。
1 |
|
方法引用
方法引用可以看作是特定场景下 Lambda 表达式的一种语法糖。当一个 Lambda 表达式的全部内容只是在调用一个已经存在的方法时,提供一种更简洁的写法来直接引用那个方法。
1 |
|
这个lambda函数本质上就是接收一个参数 s,然后将这个参数 s 原封不动地传递给了 System.out.println() 方法。
1 |
|
静态方法引用:
ClassName::staticMethodName
str -> Integer.parseInt(str)
可以写成Integer::parseInt
1
2
3
4
5List<String> stringNumbers = Arrays.asList("1", "2", "3");
List<Integer> numbers = stringNumbers.stream()
.map(Integer::parseInt) // 代替 s -> Integer.parseInt(s)
.collect(Collectors.toList());
// numbers is [1, 2, 3]指向特定对象的实例方法引用:
instance::instanceMethodName
x -> System.out.println(x)
可以写成System.out::println
1
2List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println); // 代替 name -> System.out.println(name)指向任意类型实例方法引用:
ClassName::instanceMethodName
语法:(arg0, rest_of_args) -> arg0.instanceMethodName(rest_of_args)
(s1, s2) -> s1.compareTo(s2)
可以写成String::compareTo
1
2
3
4
5
6List<String> list = Arrays.asList("a", "", "c");
Predicate<String> isEmptyPredicate = String::isEmpty; // 目标类型是 Predicate<String>
// 其抽象方法是 boolean test(String s)
// 映射为 s -> s.isEmpty()
boolean hasEmptyString = list.stream().anyMatch(isEmptyPredicate); // true构造函数引用:
ClassName::new
() -> new ArrayList<>()
可以写成ArrayList::new
1
2
3
4
5
6
7
8
9
10class Person {
String name;
public Person(String name) { this.name = name; }
// ...
}
List<String> names = Arrays.asList("Alice", "Bob");
List<Person> people = names.stream()
.map(Person::new) // 代替 name -> new Person(name)
.collect(Collectors.toList());