使用Record 建模不可变数据

用不可变类建模不可变数据

Java语言提供了几种创建不可变类的方法。最简单的方法可能是创建一个final类,其中包含final字段和一个初始化这些字段的构造函数。下面是这样一个类的例子。

public class Point {
    private final int x;
    private final int y;
    
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

现在已经编写了这些元素,接下来需要为字段添加访问器。您还将添加一个toString()方法和一个equals()方法以及一个hashCode()方法。手工编写所有这些方法非常繁琐且容易出错,幸运的是,您的IDE可以为您生成这些方法。

如果需要将该类的实例从一个应用程序传递到另一个应用程序(通过网络或文件系统发送),还可以考虑将该类设置为可序列化的。如果这样做,可能需要添加一些关于该类实例如何序列化的信息。JDK提供了几种控制序列化的方法。

最后,您的Point类可能有100行长,主要由您的IDE生成的代码填充,只是为了建模需要写入文件的两个整数的不可变聚合。

已经在JDK中添加了一些Record 来改变这一点。Record 只用一行代码就能提供所有这些信息。你所需要做的就是申报一份状态Record ;其余的由编译器生成。

使用Record 来帮忙

这里的Record 可以帮助您简化代码。从Java SE 14开始,您可以编写以下代码。

public record Point(int x, int y) {}

这一行代码为您创建了以下元素。

  • 它是一个不可变类,有两个字段:x和y,类型为int。
  • 它有一个规范的构造函数来初始化这两个字段。
  • 编译器已经为您创建了toString()、equals()和hashCode()方法,其默认行为与IDE将生成的行为相对应。如果需要,您可以通过添加这些方法的自己的实现来修改此行为。
  • 它实现了Serializable接口,这样您就可以通过网络或文件系统将Point的实例发送给其他应用程序。

在没有任何IDE的帮助下,记录使不可变的数据聚合的创建变得更加简单。它降低了出现bug的风险,因为每次修改记录的组件时,编译器都会自动为您更新equals()和hashCode()方法。

Record类

Record是用record关键字而不是class关键字声明的类。让我们定义以下Record。

public record Point(int x, int y) {}

当您创建Record时,编译器为您创建的类是final。
这个类扩展了java.lang.Record类。所以你的记录不能扩展任何类。
一条Record可以实现任何接口。

声明Record的组件

紧挨着record 名后面的块是(int x, int y),它声明了名为Point的记录的组件。对于记录的每个组件,编译器创建一个与该组件同名的私有final字段。您可以在一个记录中声明任意数量的组件。
在这个例子中,编译器创建了两个int类型的私有final字段:x和y,对应于您声明的两个组件。

不能添加到Record中的东西

有三件事你不能添加到记录中:

  • 您不能在记录中声明任何实例字段。您不能添加任何不对应于组件的实例字段。您可以创建静态字段或静态初始化器。
  • 不能定义任何字段初始值设定项。
  • 您不能添加任何实例初始化器。您可以添加静态初始化器。

使用其规范构造函数构造一个Record

编译器还为您创建一个称为规范构造函数的构造函数。此构造函数将记录的组件作为参数,并将它们的值复制到Record类的字段中。
在某些情况下,您需要覆盖这个默认行为。让我们检查两个用例:

  • 您需要验证记录的状态
  • 您需要创建一个可变组件的防御性副本。

使用紧凑的规范构造函数

可以使用两种不同的语法来重新定义Record的规范构造函数。您可以使用紧凑构造函数或规范构造函数本身。
假设您有以下Record。

public record Range(int start, int end) {}

对于这个名字的记录,可以认为结束比开始更大。您可以通过在记录中编写紧凑的构造函数来添加验证规则。

public record Range(int start, int end) {
    
    public Range {
        if (end <= start) {
            throw new IllegalArgumentException("End cannot be lesser than start");
        }
    }
}

紧凑的规范构造函数不需要声明它的参数块。注意,如果选择此语法,则不能分配此记录的字段。所有这些代码都是由编译器添加的。

使用规范构造函数

在某些情况下,您可能需要为记录的字段分配一个值。在这种情况下,您可以自己定义规范构造函数,如下面的示例所示。

public record Range(int start, int end) {
    
    public Range(int start, int end) {
        if (end <= start) {
            throw new IllegalArgumentException("End cannot be lesser than start");
        }
        if (start < 0) {
            this.start = 0;
        } else {
            this.start = start;
        }
        if (end > 100) {
            this.end = 10;
        } else {
            this.end = end;
        }
    }
}

在这种情况下,您编写的构造函数需要为记录的字段赋值。

定义任何构造函数

您还可以向记录添加任何构造函数,只要该构造函数调用记录的规范构造函数。该语法与用另一个构造函数调用一个构造函数的经典语法相同。对于任何类,对this()的调用必须是构造函数的第一个语句。
让我们审查以下州记录。它定义在三个组件上:

  • 这个州的名字
  • 这个州首府的名字
  • 城市名称列表,可能为空。

我们需要存储城市名单的防御拷贝,以确保它不会从这个记录之外被修改。这是通过重新定义规范构造函数来实现的。

在应用程序中,不使用任何城市的构造函数是很有用的。这可以是另一个构造函数,它只接受州名和首都城市名。第二个构造函数必须调用规范构造函数。

然后,您可以将这些城市作为变量传递,而不是传递一个城市列表。为此,您可以创建第三个构造函数,该构造函数必须使用适当的列表调用规范构造函数。

public record State(String name, String capitalCity, List<String> cities) {

    public State(String name, String capitalCity, List<String> cities) {
        this.name = name;
        this.capitalCity = capitalCity;
        this.cities = List.copyOf(cities);
    }

    public State(String name, String capitalCity) {
        this(name, capitalCity, List.of());
    }

    public State(String name, String capitalCity, String... cities) {
        this(name, capitalCity, List.of(cities));
    }
}

获取一个记录的状态

您不需要向记录添加任何访问器,因为编译器会为您做这件事。一条记录每个组件有一个访问器方法,该方法具有该组件的名称。
本教程第一部分中的Point记录有两个访问器方法:x()和y(),它们返回相应组件的值。

在某些情况下,您需要定义自己的访问器。例如,前一节的国家记录需要制作一份城市清单的防御性副本。您可以在状态记录中添加以下代码以返回这个防御副本。

public List<String> cities() {
    return List.copyOf(cities);
}

在真实用例中使用Records

记录是一个通用的概念,可以在许多上下文中使用。
第一个是在应用程序的对象模型中携带数据。您可以将记录用于它们的设计目的:充当不可变的数据载体。
因为可以声明本地记录,所以还可以使用它们来提高代码的可读性。
让我们考虑下面的用例。您有两个建模为记录的实体:City和State。

public record City(String name, State state) {} 

public record State(String name) {} 

假设您有一个城市列表,您需要计算拥有最多城市数量的州。您可以使用Stream API首先构建州的直方图,其中包含每个州拥有的城市数量。这个直方图是用Map建模的。

List<City> cities = List.of();

Map<State, Long> numberOfCitiesPerState = 
    cities.stream()
          .collect(Collectors.groupingBy(
                   City::state, Collectors.counting()
          ));

得到这个直方图的最大值是下面的通用代码。

Map.Entry<State, Long> stateWithTheMostCities =
    numberOfCitiesPerState.entrySet().stream()
                          .max(Map.Entry.comparingByValue())
                          .orElseThrow();

最后一段代码是技术性的;不含任何商业意义;因为是使用Map.Entry实例来建模直方图的每个元素。

使用本地记录可以极大地改善这种情况。下面的代码创建了一个新的记录类,它聚合了一个州和这个州中的城市数量。它有一个接受Map.Entry 作为参数的构造函数,以将键值对流映射到记录流。

因为需要按城市数量比较这些聚合,所以可以添加一个工厂方法来提供这个比较器。代码如下所示。

record NumberOfCitiesPerState(State state, long numberOfCities) {

    public NumberOfCitiesPerState(Map.Entry<State, Long> entry) {
        this(entry.getKey(), entry.getValue());
    }

    public static Comparator<NumberOfCitiesPerState> comparingByNumberOfCities() {
        return Comparator.comparing(NumberOfCitiesPerState::numberOfCities);
    }
}

NumberOfCitiesPerState stateWithTheMostCities =  
    numberOfCitiesPerState.entrySet().stream()
                          .map(NumberOfCitiesPerState::new)
                          .max(NumberOfCitiesPerState.comparingByNumberOfCities())
                          .orElseThrow();

现在,代码以有意义的方式提取max。您的代码更具可读性,更容易理解,更不容易出错,从长远来看更容易维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值