Java编程笔记31:Record

news2025/1/16 13:42:42

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;
    }

    @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",
                "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.coloradmin.cn/o/620042.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

uniapp开发小程序-显示左滑删除效果

一、效果图&#xff1a; 二、代码实现&#xff1a; <template><view class"container"><view class"myorderList"><uni-swipe-action><uni-swipe-action-item class"swipe-action-item" :right-options"option…

【严重】Nacos 集群Raft反序列化漏洞

漏洞描述 Nacos是一个用于动态服务发现和配置以及服务管理的平台。 攻击者可以在Nacos集群处理某些Jraft请求时利用Hessian进行无限制的反序列化&#xff0c;从而造成远程代码执行。 由于Nacos默认监听7848端口处理Raft协议请求&#xff0c;攻击者可能通过向7848端口发送恶意…

[笔记]C++并发编程实战 《四》同步并发操作

文章目录 前言第4章 同步并发操作4.1 等待一个事件或其他条件4.1.1 等待条件达成4.1.2 使用条件变量构建线程安全队列 4.2 使用期望值等待一次性事件4.2.1 后台任务的返回值4.2.2 任务与期望值关联4.2.3 使用(std::)promises4.2.4 将异常存与期望值中4.2.5 多个线程的等待 前言…

【Mysql】InnoDB 中的聚簇索引、二级索引、联合索引

一、聚簇索引 其实之前内容中介绍的 B 树就是聚簇索引。 这种索引不需要我们显示地使用 INDEX 语句去创建&#xff0c;InnoDB 引擎会自动创建。另外&#xff0c;在 InnoDB 引擎中&#xff0c;聚簇索引就是数据的存储方式。 它有 2 个特点&#xff1a; 特点 1 使用记录主键…

【每日挠头算法题(2)】压缩字符串|仅执行一次字符串交换能否使两个字符串相等

文章目录 一、压缩字符串思路 二、仅执行一次字符串交换能否使两个字符串相等思路1&#xff1a;计数法思路2&#xff1a;模拟法 总结 一、压缩字符串 点我直达~ 思路 使用双指针法 大致过程如下&#xff1a; 使用双指针&#xff0c;分别读&#xff08;read&#xff09;&…

mPak的使用文档

mpak介绍 mpak是一种文件格式&#xff0c;同时也是一款虚幻引擎插件&#xff0c;该插件提供了打包、解析和挂载mPak文件的方法&#xff0c;将不同平台的软件包和未编译的资源集成到mPak文件中&#xff0c;该文件具有跨平台兼容性。它支持不同阶段的挂载&#xff0c;例如在编辑…

monaco-editor插件自定义编辑器内容颜色

开始之前先说一下他的自定义内容颜色的api monaco.languages.setMonarchTokensProvider((languageId: string, languageDef: IMonarchLanguage | Thenable<IMonarchLanguage>)) 参数解析&#xff1a;接收两个参数 一个是你要设置的编辑器语言种类&#xff0c;可以是sq…

Ubuntu14.04安装igH EtherCAT Master

一、下载EtherCAT安装包 安装包下载路径&#xff1a;EtherLab EtherCAT Master / Code / [334c34]&#xff0c;打开后点击Download下载 二、安装前置依赖库 最好切换到root用户进行下列步骤 apt-get install autoconf automake libtool net-tools三、编译安装 解压安装包&a…

excel如何折叠展开行列?

Excel可以使用分组功能来实现折叠展开行列的效果&#xff0c;同时可以在单元格内添加号或-号来进行操作。 具体步骤如下&#xff1a; 1. 选中需要进行折叠展开的行或列&#xff0c;右键选择“分组”。 2. 在弹出的“分组”对话框中&#xff0c;选择“行”或“列”&#xff0…

『赠书活动 | 第十一期』清华社赞助 | 《Python系列丛书》

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 『赠书活动 &#xff5c; 第十一期』 本期书籍&#xff1a;《Python系列丛书》 公众号赠书&#xff1a;第三期 参与方式&#xff1a;关注公众号&#xff1a;低调而奢…

逻辑回归分类器-创建词向量-情感分析

题目 请使用您今天学习的逻辑回归分类器对下面的聊天机器人数据进行自动分类。&#xff08;3分&#xff09; https://github.com/songys/Chatbot_data&#xff08;ChatbotData.csv文件&#xff09; https://raw.githubusercontent.com/songys/Chatbot_data/master/ChatbotDa…

【职业人生】如何有效的在职场当中避免工作失误和提高个人发展

《左传宣公二年》&#xff1a;“人谁无过&#xff0c;过而能改&#xff0c;善莫大焉。”古往今来&#xff0c;多少人犯过错误。强大如“智绝”的诸葛孔明&#xff0c;也有街亭之失。职场人更是难免会在工作中出现失误。 在职场生涯当中避免不了在工作当中带来的失误&#xff0c…

【Hadoop综合实践】手机卖场大数据综合项目分析

&#x1f680; 本文章实现了基于MapReduce的手机浏览日志分析 &#x1f680; 文章简介&#xff1a;主要包含了数据生成部分&#xff0c;数据处理部分&#xff0c;数据存储部分与数据可视化部分 &#x1f680; 【本文仅供参考】其中需求实现的方式有多种&#xff0c;提供的代码并…

从0到1搞定在线OJ

目录 一、在线OJ的的原理 二、在线OJ的使用规则 三、注意事项 1.关于作弊 2.如何防止作弊 3.输入输出格式 4.换行问题 四、经典在线OJ坑人题目以及博主被坑经历 五、提交不成功及解决方法 六、如何得心应手的拿下OJ系统 七、在线OJ的骗分技巧 在线OJ&#xff08;Onl…

管理项目-查询数据

人事管理项目-查询数据模块 后端实现配置文件实体类Dao层测试前端实现1&#xff0e;创建Dept页面2&#xff0e;修改路由3 测试 后端实现 配置文件 在application.yml文件中配置数据库连接、JPA及端口等信息&#xff0c;代码如下&#xff1a; 实体类 配置完成后建立和表结构…

“出海热”仍在持续,进军东南亚市场谁能率先突围?

在油改电的趋势下&#xff0c;伴随钠电池车型的推出&#xff0c;电动两轮车市场被进一步激活。据艾瑞咨询不完全统计与估算&#xff0c;2022年国内两轮电动车销量约5010万辆&#xff0c;较去年增长15.2%&#xff0c;预计2023年销量达到5400万辆。持续增长的销量足以说明当下的国…

JeecgBoot低代码平台 3.5.2,仪表盘版本发布!重磅新功能—支持在线拖拽设计大屏和门户

项目介绍 JeecgBoot是一款企业级的低代码平台&#xff01;前后端分离架构 SpringBoot2.x&#xff0c;SpringCloud&#xff0c;Ant Design&Vue3&#xff0c;Mybatis-plus&#xff0c;Shiro&#xff0c;JWT 支持微服务。强大的代码生成器让前后端代码一键生成! JeecgBoot引领…

北京君正案例:数传网关的集大成者—积木式边缘网关

数传网关的集大成者 USR-M300产品集成了数据的边缘采集、计算、主动上报和数据读写&#xff0c;联动控制&#xff0c;IO采集和控制等功能&#xff0c;采集协议包含标准Modbus协议和多种常见的PLC协议&#xff0c;以及行业专用协议&#xff1b;主动上报采用分组上报方式&#xf…

如何靠自学成为一名网络安全工程师?

1. 前言 说实话&#xff0c;一直到现在&#xff0c;我都认为绝大多数看我这篇文章的读者最后终究会放弃&#xff0c;原因很简单&#xff0c;自学终究是一种适合于极少数人的学习方法&#xff0c;而且非常非常慢&#xff0c;在这个过程中的变数过大&#xff0c;稍有不慎&#…

电脑提示d3dcompiler_47.dll缺失怎么修复?

d3dcompiler_47.dll是 Microsoft 的 DirectX 11 核心组件之一&#xff0c;它主要用于编译和运行 Direct3D 11 应用程序和游戏。如果您的系统中缺少这个 DLL 文件&#xff0c;可能会导致一些程序无法正常运行&#xff0c;很多游戏跟图形处理软件都会运用到。如果电脑提示“找不到…