Java编程笔记31:Record

news/2024/10/21 5:53:22/

Java编程笔记31:Record

image-20221101145143893

图源: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;}@Overridepublic 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());}@Overridepublic int hashCode() {return Objects.hash(getName(), getAge());}@Overridepublic 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","123@qq.com","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("123@qq.com").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

http://www.ppmy.cn/news/262218.html

相关文章

ldap服务安装,客户端安装,ldap用户登录验证测试

安装服务端 # 安装ldap服务 docker run -p 389:389 -p 636:636 \ --name openldap \-v /home/manager/testldap:/testldap \ --env LDAP_ORGANISATION"admin" \ --env LDAP_DOMAIN"hadoop.apache.org" \ --env LDAP_ADMIN_PASSWORD"Dmpxxx" \ -…

三菱FX系列 DZRN指令使用

1、 指令功能&#xff1a;DZRN K20000 K3000 X012 Y000 这是一个回原点指令&#xff0c;K20000表示刚开始回原点的脉冲频率&#xff0c;当检测到X12的上升沿后&#xff0c;脉冲输出频率降为3000&#xff08;题目中以K3000表示&#xff09;。当再检测到X12的下降沿后&#xff0c…

三菱FX系列DPLSV指令使用

1、 DPLSV指令介绍&#xff1a;PLSV是可变脉冲输出指令&#xff0c;有3个参数&#xff0c;依次是输出脉冲频率&#xff0c;脉冲输出地址&#xff0c;方向输出地址。 例如&#xff1a;DPLSV K-1000 Y0 Y1 就是驱动y0 输出1000hz频率的脉冲 Y1控制方向 修改数值的正负也能改变方向…

三菱FX3U 485BD与3台施耐德ATV 71变频器通讯程序

三菱FX3U 485BD与3台施耐德ATV 71变频器通讯程序 程序为原创&#xff0c;稳定可靠&#xff0c;有注释。 并附送程序&#xff0c;有接线方式&#xff0c;设置。 同时实现变频器 DRIVECOM流程&#xff0c;解决施耐德ATV变频器断电重启后&#xff0c;自准备工作&#xff0c;程序稳…

三菱FX3U与4台三菱变频器专用指令通讯案例

三菱FX3U与4台三菱变频器专用指令通讯案例 功能&#xff1a;采用三菱FX3U PLC与4台三菱变频器E740进行通讯 配件&#xff1a;三菱FX3U的PLC&#xff0c;加FX3U 485BD板。 三菱E740变频器。 昆仑通态触摸屏 方式&#xff1a;采用三菱变频器专用通讯指令。 效果&#xff1a;控…

三菱J4伺服驱动器拨码

目的&#xff0c;在接好线的伺服驱动器&#xff0c;有四个伺服&#xff0c;则需要拨站&#xff0c;分别为0,1,2,3,4, 1、 拨站位置&#xff1a; SW1分别指向0,1,2,3对应1,2,3,4站 。sw2不用拨 在这里插入图片描述 2、断电重启复位&#xff1a; 3、拨站成功后&#xff0c;若不…

西门子200smart与施耐德ATV变频器modbus通讯 西门子s7-200smart与施耐德ATV12变频器通讯

西门子200smart与施耐德ATV变频器modbus通讯 西门子s7-200smart与施耐德ATV12变频器通讯&#xff0c;可靠稳定&#xff0c;同时解决施耐德ATV变频器断电重启后&#xff0c;自准备工作&#xff0c;无需人为准备。 器件&#xff1a;西门子s7-200smart PLC&#xff0c;昆仑通态带以…

三菱FX3U +485 ADP与施耐德ATV-71变频器通讯程序 同时实现变频器 DRIVECOM流程,解决施耐德ATV变频器断电重启后,自准备工作

三菱FX3U 485 ADP与施耐德ATV-71变频器通讯程序 程序为原创&#xff0c;稳定可靠&#xff0c;有注释。并附送程序&#xff0c;有接线方式&#xff0c;设置。 同时实现变频器 DRIVECOM流程&#xff0c;解决施耐德ATV变频器断电重启后&#xff0c;自准备工作&#xff0c;程序稳定…