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 项目地址


原始标题:Currying in Java | Baeldung