1. 简介
自 Java 8 开始,我们可以定义接受一到两个参数的函数,并将这些函数作为行为注入到其他函数中。但对于参数更多的函数,我们往往需要借助外部库,例如 Vavr。
另一种方式是使用 柯里化(Currying) 技术。结合柯里化和 函数式接口,我们甚至可以构建出易读、强制输入所有必要参数的 Builder 模式。
✅ 本文将介绍什么是柯里化,以及它在 Java 中的实际应用。
2. 简单示例
假设我们要创建一个信件对象,它包含多个参数。
我们先从一个简化版本开始,只需要称呼和正文:
class Letter {
private String salutation;
private String body;
Letter(String salutation, String body){
this.salutation = salutation;
this.body = body;
}
}
2.1. 通过方法创建
我们可以用一个普通方法来创建对象:
Letter createLetter(String salutation, String body){
return new Letter(salutation, body);
}
2.2. 使用 BiFunction 创建
虽然上面的方法可以工作,但如果我们需要将这个行为传入一个函数式风格的 API,Java 8 提供了 BiFunction
来处理两个参数的函数:
BiFunction<String, String, Letter> SIMPLE_LETTER_CREATOR
= (salutation, body) -> new Letter(salutation, body);
2.3. 使用一系列单参数函数
我们也可以将这个函数转换为一系列只接受一个参数的函数:
Function<String, Function<String, Letter>> SIMPLE_CURRIED_LETTER_CREATOR
= salutation -> body -> new Letter(salutation, body);
可以看到,salutation
映射到一个函数,该函数再映射到最终的 Letter
对象。注意返回类型从 BiFunction
变成了嵌套的 Function
类型。这种将多参数函数转换为一系列单参数函数的技术,就叫做 柯里化(Currying)。
3. 高级示例
为了展示柯里化的真正优势,我们给 Letter
类添加更多字段:
class Letter {
private String returningAddress;
private String insideAddress;
private LocalDate dateOfLetter;
private String salutation;
private String body;
private String closing;
Letter(String returningAddress, String insideAddress, LocalDate dateOfLetter,
String salutation, String body, String closing) {
this.returningAddress = returningAddress;
this.insideAddress = insideAddress;
this.dateOfLetter = dateOfLetter;
this.salutation = salutation;
this.body = body;
this.closing = closing;
}
}
3.1. 方法创建对象
和之前一样,我们可以使用一个方法来创建对象:
Letter createLetter(String returnAddress, String insideAddress, LocalDate dateOfLetter,
String salutation, String body, String closing) {
return new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
}
3.2. 任意参数数量的函数
Java 原生支持的函数式接口只支持 0 参数(Supplier
)、1 参数(Function
)和 2 参数(BiFunction
)。对于超过两个参数的函数,我们需要借助柯里化将其转换为多个单参数函数的链式调用。
比如,我们把上面的六参数构造函数柯里化:
Function<String, Function<String, Function<LocalDate, Function<String,
Function<String, Function<String, Letter>>>>>> LETTER_CREATOR =
returnAddress
-> closing
-> dateOfLetter
-> insideAddress
-> salutation
-> body
-> new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
3.3. 类型冗长问题
显而易见,上述类型声明非常冗长,难以阅读。调用时也需要连续使用 apply
六次:
LETTER_CREATOR
.apply(RETURNING_ADDRESS)
.apply(CLOSING)
.apply(DATE_OF_LETTER)
.apply(INSIDE_ADDRESS)
.apply(SALUTATION)
.apply(BODY);
3.4. 预填充参数
通过函数链,我们可以创建一个“预填充”工具函数,提前传入部分参数,返回一个待完成的函数:
Function<String, Function<LocalDate, Function<String, Function<String, Function<String, Letter>>>>>
LETTER_CREATOR_PREFILLED = returningAddress -> LETTER_CREATOR.apply(returningAddress).apply(CLOSING);
⚠️ 为了使预填充更有效,我们需要合理安排参数顺序,把通用性更强的参数放在前面。
4. 结合 Builder 模式
为了解决类型冗长和多次调用 apply
的问题,我们可以使用 Builder 模式 来增强可读性:
AddReturnAddress builder(){
return returnAddress
-> closing
-> dateOfLetter
-> insideAddress
-> salutation
-> body
-> new Letter(returnAddress, insideAddress, dateOfLetter, salutation, body, closing);
}
这里我们不再使用函数链,而是使用一组自定义的函数式接口。注意返回类型是 AddReturnAddress
。接下来我们定义这些接口:
interface AddReturnAddress {
Letter.AddClosing withReturnAddress(String returnAddress);
}
interface AddClosing {
Letter.AddDateOfLetter withClosing(String closing);
}
interface AddDateOfLetter {
Letter.AddInsideAddress withDateOfLetter(LocalDate dateOfLetter);
}
interface AddInsideAddress {
Letter.AddSalutation withInsideAddress(String insideAddress);
}
interface AddSalutation {
Letter.AddBody withSalutation(String salutation);
}
interface AddBody {
Letter withBody(String body);
}
使用这种方式创建对象非常直观:
Letter.builder()
.withReturnAddress(RETURNING_ADDRESS)
.withClosing(CLOSING)
.withDateOfLetter(DATE_OF_LETTER)
.withInsideAddress(INSIDE_ADDRESS)
.withSalutation(SALUTATION)
.withBody(BODY));
我们也可以预填充部分参数:
AddDateOfLetter prefilledLetter = Letter.builder().
withReturnAddress(RETURNING_ADDRESS).withClosing(CLOSING);
✅ 这些接口强制了参数的输入顺序,确保不会漏填或错填参数。
5. 总结
本文展示了如何使用柯里化绕过 Java 函数式接口对参数数量的限制,并实现参数预填充。同时,我们还演示了如何将柯里化与 Builder 模式结合,构建出既灵活又易读的对象创建方式。
完整代码示例可以在这里找到:GitHub 项目地址。