《Java的函数式》第五章:使用Records

147 阅读4分钟

第五章:使用Records

Java 14引入了一种新的数据结构类型,作为预览功能,经过两个版本的完善后正式发布:Records。Records不仅仅是你可以使用的另一种典型Java类型或技术。相反,Records是一种全新的语言特性,为您提供了一个简单但功能丰富的数据聚合器,减少了冗余代码的编写。

数据聚合类型

从一个普遍的角度来看,数据聚合是从多个来源收集数据并以更适合预期目的和更可取的使用方式组装数据的过程。可能最为人所熟知的数据聚合类型是元组。

元组(Tuples)

从数学的角度来说,元组是一种“有限有序元素序列”。在编程语言中,元组是一种聚合多个值或对象的数据结构。

元组分为两种类型。结构元组仅依赖于包含元素的顺序,因此只能通过索引访问,如下所示的 Python 代码所示:

apple = ("apple", "green")
banana = ("banana", "yellow")
cherry = ("cherry", "red")

fruits = [apple, banana, cherry]

for fruit in fruits:
  print "The", fruit[0], "is", fruit[1]

命名元组不使用索引来访问其数据,而是使用组件名称,如下所示的 Swift 代码所示:

typealias Fruit = (name: String, color: String)

let fruits: [Fruit] = [  (name: "apple", color: "green"),  (name: "banana", color: "yellow"),  (name: "cherry", color: "red")]

for fruit in fruits {
  println("The (fruit.name) is (fruit.color)")
}

为了演示Records的功能,首先让我们看看如何从经典的POJO(Plain Old Java Object)转换为不可变对象,然后我将展示如何使用Record来复制相同的功能。

简单的POJO

首先,让我们来看一下Java中数据聚合的“Record之前”的状态,以更好地理解Records提供了什么。作为一个示例,我们创建一个简单的“用户”类型作为“经典”的POJO,将其演变为“不可变”的POJO,最后变成一个Record。这将是一个简单的类型,包含用户名、活动状态、上次登录时间戳以及在典型的Java代码中常见的样板代码,如示例5-1所示。

public final class User {

  private String username;
  private boolean active;
  private LocalDateTime lastLogin;

  public User() { } 

  public User(String username,
              boolean active,
              LocalDateTime lastLogin) { 
    this.username = username;
    this.active = active;
    this.lastLogin = lastLogin;
  }

  public String getUsername() { 
    return this.username;
  }

  public void setUsername(String username) { 
    this.username = username;
  }

  public boolean isActive() { 
    return this.active;
  }

  public void setActive(boolean active) { 
    this.active = active;
  }

  public LocalDateTime getLastLogin() { 
    return this.lastLogin;
  }

  public void setLastLogin(LocalDateTime lastLogin) { 
    this.lastLogin = lastLogin;
  }

  @Override
  public int hashCode() { 
    return Objects.hash(this.username, this.active, this.lastLogin);
  }

  @Override
  public boolean equals(Object obj) { 
    if (this == obj) {
      return true;
    }

    if (obj == null || getClass() != obj.getClass()) {
      return false;
    }

    User other = (User) obj;
    return Objects.equals(this.username, other.username)
           && this.active == other.active
           && Objects.equals(this.lastLogin, other.lastLogin);
  }

  @Override
  public String toString() { 
    return new StringBuilder().append("User [username=")
                              .append(this.username)
                              .append(", active=")
                              .append(this.active)
                              .append(", lastLogin=")
                              .append(this.lastLogin)
                              .append("]")
                              .toString();
  }
}

包括空行和大括号,这个示例需要超过70行代码来仅仅保存三个数据字段。难怪Java最常见的抱怨之一就是其冗长和“过多的仪式感”,即要做标准事情时需要进行太多的操作!

现在,让我们将其转换为一个不可变的POJO。

从POJO到不可变性

使User POJO成为不可变的会稍微减少所需的样板代码,因为你不再需要任何setter方法,如示例5-2所示。

public final class User {

  private final String username; 
  private final boolean active;
  private final LocalDateTime lastLogin;

  public User(String username,
              boolean active,
              LocalDateTime lastLogin) { 
    this.username = username;
    this.active = active;
    this.lastLogin = lastLogin;
  }

  public String getUsername() { 
    return this.username;
  }

  public boolean isActive() { 
    return this.active;
  }

  public LocalDateTime getLastLogin() { 
    return this.lastLogin;
  }

  @Override
  public int hashCode() { 
    // UNCHANGED
  }

  @Override
  public boolean equals(Object obj) { 
    // UNCHANGED
  }

  @Override
  public String toString() { 
    // UNCHANGED
  }
}

通过自己使类型成为不可变的,只能删除setter和空构造函数的代码;其他的代码仍然存在。对于仅持有三个字段且没有太多附加功能的情况来说,这仍然是很多代码。当然,我们可以删除更多的“仪式感”,使用一个具有三个公共final字段和一个构造函数的简单类。根据你的需求,这可能已经“刚刚好”。然而,额外的功能,如相等比较和正确的hashCode以便在Set或HashMap中使用,或者合理的toString输出,都是可取的特性。

从POJO到Record

最后,让我们来看一个更一般化、不那么繁琐但功能丰富的解决方案,使用Record来实现:

public record User(String username,
                   boolean active,
                   LocalDateTime lastLogin) {
  // NO BODY
}

就是这样了。

User Record 具有与不可变的 POJO 相同的功能。它是如何用如此少的代码实现这么多功能的将在接下来的章节中详细解释。