Java编程笔记31:Record

Java编程笔记31:Record

Java编程笔记31:Record_第1张图片

图源:Fotor懒设计

在日常使用的时候,我们往往需要创建一些“仅用于传输数据的类型”,比如Web编程时候的DTO。

将特殊用途的类型限制为“只读”的一个好处是,这些类型可以安全地在多线程之间共享,并且在涉及计算哈希值的时候,不用担心这些对象因为内部属性改变导致哈希值改变。

为什么要使用 Record

如果要创建一个“只读”类型,通常我们需要这样做:

public class Person1 {
    private final String name;
    private final Integer age;

    public Person1(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person1 person1 = (Person1) o;
        return Objects.equals(getName(), person1.getName()) && Objects.equals(getAge(), person1.getAge());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName(), getAge());
    }

    @Override
    public String toString() {
        return "Person1{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }
}

我们需要做的是:

  1. 将属性设置为private final
  2. 添加一个包含所有属性的构造器。
  3. 为属性添加Getter。
  4. 添加hashCodeequalstoString方法。

虽然大多数工作都可以借助IDE来完成,但是仍然需要话费一点时间在“样板代码”上,并且如果这个类型需要添加一些属性,我们还需要话费时间修改相应的代码。

在之前的文章中,我介绍了一个工具 Lombok,借助它我们可以改善此类的代码:

@Value
public class Person2 {
    String name;
    Integer age;
}

Lombok 可以帮助我们实现之前示例中的“样板代码”,我们只需要使用一个@Value注解。

关于 Lombok 的更多介绍,可以阅读我的另一篇文章。

从 JDK14 开始,我们多了一种选项——使用Record

public record Person(String name, Integer age) {
}

看起来这里用record代替了class,但实际上record并不是一个关键字,只是一个包类型,这是官方出于某种向前兼容的考虑。

查看Person对应的字节码:

public record Person(String name, Integer age) {
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String name() {
        return this.name;
    }

    public Integer age() {
        return this.age;
    }
}

可以看到Person在生成字节码后,由编译器生成了构造器和Getter。实际上相应的hashCodetoStringequals同样可用:

Person person = new Person("icexmoon", 12);
System.out.println(person);
Person person2 = new Person("icexmoon", 20);
System.out.println(person.equals(person2));
Person person3 = new Person("icexmoon", 12);
System.out.println(person.equals(person3));
// Person[name=icexmoon, age=12]
// false
// true

构造器

通常我们无需为Record指定构造器,使用其默认创建的构造器即可。如果我们需要为默认生成的构造器添加某些处理逻辑,可以:

public record Person(String name, Integer age) {
    public Person{
        Objects.requireNonNull(name);
        name = name.trim();
        if ("".equals(name)){
            throw new RuntimeException("name 不能为空");
        }
        if (age <=0 || age >=150){
            throw new RuntimeException("age 的值非法");
        }
    }
}

生成的字节码:

public record Person(String name, Integer age) {
    public Person(String name, Integer age) {
        Objects.requireNonNull(name);
        name = name.trim();
        if ("".equals(name)) {
            throw new RuntimeException("name 不能为空");
        } else if (age > 0 && age < 150) {
            this.name = name;
            this.age = age;
        } else {
            throw new RuntimeException("age 的值非法");
        }
    }
    // ...
}

这里的不带参数列表的构造器public Person {...}可以称作“紧凑构造器”(compact constructor),虽然没有显式声明参数列表,但我们依然可以直接使用属性名称命名的参数,并且无需添加属性赋值语句(比如this.name=name),生成字节码的时候编译器会自动添加。

当然也可以用传统方式编写构造器:

public record Person(String name, Integer age) {
    public Person(String name, Integer age){
        Objects.requireNonNull(name);
        name = name.trim();
        if ("".equals(name)){
            throw new RuntimeException("name 不能为空");
        }
        if (age <=0 || age >=150){
            throw new RuntimeException("age 的值非法");
        }
        this.name = name;
        this.age = age;
    }
}

这同样是合法的,但并不推荐。

注意,传统方式最后的属性赋值语句。

需要注意的是,紧凑构造器和传统构造器不能共存:

public record Person(String name, Integer age) {
    public Person{

    }
    public Person(String name, Integer age){
		// ...
    }
}

上边的示例无法通过编译。

这是可以理解的,两个构造器本质上完全相同,编译器并不知道该使用哪一个。

当然,重载构造器以提供多样的对象创建方式是被允许的:

public record Person(String name, Integer age) {
    public Person {
        // ...
    }

    public Person(String name){
        this(name, 10);
    }
}

调用:

Person person3 = new Person("icexmoon");
System.out.println(person3);
// Person[name=icexmoon, age=10]

静态属性和方法

同样的,可以在record中使用静态属性和方法:

public record Person(String name, Integer age) {
    private static final int DEFAULT_AGE = 10;
    private static final String DEFAULT_NAME = "icexmoon";

    public Person {
        // ...
    }

    public Person(String name) {
        this(name, DEFAULT_AGE);
    }

    public static Person defaultPerson() {
        return new Person(DEFAULT_NAME, DEFAULT_AGE);
    }
}

示例

这里看一个实际示例,如何在Web应用中使用record

public record Result<T>(boolean successFlag, String errorCode, String errorMsg, T data) {
    private static final String SUCCESS_CODE = "success";

    public Result {
        Objects.requireNonNull(errorCode);
        Objects.requireNonNull(errorMsg);
        errorCode = errorCode.trim();
        if ("".equals(errorCode)) {
            throw new RuntimeException("errorCode 不能为空");
        }
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(true, SUCCESS_CODE, "", data);
    }

    public static Result<Object> success() {
        return success(null);
    }

    public static Result<Object> fail(String errorCode, String errorMsg) {
        return new Result<>(false, errorCode, errorMsg, null);
    }
}

@RestController
@RequestMapping("/person")
@Log4j2
public class PersonController {
    private static record AddPersonDTO(@NotBlank String name,
                                       @NotNull @Range(min = 1, max = 150) Integer age){
    }

    @PostMapping("/add")
    public Result<?> addPerson(@Validated @RequestBody AddPersonDTO dto){
        //调用service,执行添加动作
        log.debug(dto);
        return Result.success();
    }
}

这里的标准返回Result和充当DTO的AddPersonDTO都使用record来创建。

  • Result中用于表示成功失败的属性命名为successFlag而非通常的success,这是因为会与静态方法success冲突(因为record默认产生的Getter同样以属性名命名),无法通过编译。

可以看到,Hibernate Validation 与 Record 同样可以很好地协同工作。

如果想了解更多的 Hibernate Validation 在 Spring 中使用的内容,可以阅读这里。

Record 和 Lombok

Record 和 Lombok 的@Value的用途是很相似的,都可以用来表示一个"只读类型"。所以讨论它们之间的异同就很有必要了。

可见性

Record 被定义为一个“透明的数据载体”,因此它的Getter和构造器都必须是public的,如果我们想让只读类型的Getter或构造器不是public,那就只能使用 Lombok。比如:

@Value
@Getter(AccessLevel.PRIVATE)
public class Person3 {
    String name;
    Integer age;

    private Person3(final String name, final Integer age) {
        this.name = name;
        this.age = age;
    }

    public static Person3 buildPerson(String name, Integer age) {
        return new Person3(name, age);
    }
}

此时,Lombok 生成的Getter都是private的,自然无法被外部调用,同时构造器同样被我们改写为private,外部只能通过静态方法buildPerson来创建对象。

可以看到,Lombok 比 Record 更灵活,我们可以根据需要修改内部构造器和方法的可见性,这点 Record 是无法做到的。

多个属性

如果类型中有多个属性,使用 Record 的代码的可读性会变差,比如:

public record Person4(String firstName,
                      String lastName,
                      Integer age,
                      List<String> hobbies,
                      String career,
                      String email,
                      String address) {
}

@SpringBootApplication
public class MyrecordApplication {
    // ...
    private static void testRecord4() {
        var p = new Person4("Jack",
                "Chen",
                15,
                List.of("singing", "drawing"),
                "actor",
                "[email protected]",
                "HK");
        System.out.println(p);
    }
}

可以使用 Lombok 编写更具可读性的代码:

@Value
@Builder
public class Person5 {
    String firstName;
    String lastName;
    Integer age;
    List<String> hobbies;
    String career;
    String email;
    String address;
}

@SpringBootApplication
public class MyrecordApplication {
	// ...
    private static void testRecord5() {
        var p = Person5.builder()
                .firstName("Jack")
                .lastName("Chen")
                .hobbies(List.of("singing", "drawing"))
                .career("actor")
                .address("HK")
                .age(15)
                .email("[email protected]")
                .build();
        System.out.println(p);
    }
}

因此,对于拥有很多属性的类型,可以可以优先考虑使用 Lombok。

继承

Record 是不能被继承的,Lombok 的@Value标记的类型同样不能被继承,但我们可以组合使用 Lombok的其它注解来更灵活地构建我们需要的类型并实现继承,比如:

@Data
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
@Setter(value = AccessLevel.NONE)
@RequiredArgsConstructor
public class Person5 {
    String firstName;
    String lastName;
    Integer age;
    List<String> hobbies;
    String career;
    String email;
    String address;
}

@Value
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Person6 extends Person5 {
    private final String country;
    public Person6(String firstName, String lastName, Integer age, List<String> hobbies, String career, String email, String address, String country) {
        super(firstName, lastName, age, hobbies, career, email, address);
        this.country = country;
    }
}

当然,这里依然有很多不便,比如子类Person6无法直接用@Value@RequiredArgsConstructor生成包含所有属性的构造器,所以这里只能通过手动创建。但至少可以通过这种方式实现继承,这点 Record 是无法做到的。

总结

总的来说,Record 的用途相对简单和直接,就是充当一个“透明的数据载体”,而 Lombok 除了直接使用@Value注解外,还可以结合其它注解更灵活地定制一个类型。

其它限制

Record 还存在一些其它限制,比如不能从其它类型扩展:

public record Person7() extends Person1 {
}

这样的代码无法通过编译,会提示“不允许 Record 从其它类型扩展”。

这是因为所有的record类型实际上都会隐式地从Record类扩展,而 Java 本身不支持多继承。

其次,Record 的属性也不能被初始化,比如:

public record Person7(String name, Integer age = 7){
}

这样的写法不被允许,因此 Record 的属性只能是通过构造器进行初始化。

The End,谢谢阅读。

本文的所有示例代码可以通过这里获取。

参考资料

  • 从零开始 Spring Boot 35:Lombok - 红茶的个人站点 (icexmoon.cn)
  • Java 14 Record Keyword | Baeldung
  • 从零开始 Spring Boot 13:参数校验 - 红茶的个人站点 (icexmoon.cn)
  • Java 14 Record vs. Lombok
  • Record (Java SE 17 & JDK 17) (oracle.com)
  • Record vs. Final Class in Java

你可能感兴趣的:(JAVA,java,record,lombok)