Java 对象字段基本类型和包装类型的选择以及 null 处理的一些思考

版本

  • JDK 8

起因

  • 最近合并代码中发现了一个有意思的报错:Lombok builder is missing non nullable fields,大意是说 Lombok 构建器缺少对非空字段的处理,下面是对情况复现 demo:
java 复制代码
public class TestMain {

    public static void main(String[] args) {
        // Lombok builder is missing non nullable fields
        Student student = Student.builder().name("xiaoming").build();
        System.out.println(student.getAge());
    }
}


@Builder
@Data
class Student {
    @NonNull
    private String name;
    private int age;
}
  • 先说结论:其实这里是 IDE 在提示我们 age 字段不为 null,你应该对字段进行处理初始化和赋值。

解决方案

方案一

  • 理论上不用解决,因为这个提示只是 IDE 层面的提示而编译器并不会实际进行检查,程序是可以正常运行的。

方案二

  • 设置 ageInteger,表示允许字段为 null
java 复制代码
private Integer age;

方案三

  • Builder.Default 设置默认值
java 复制代码
@Builder.Default
private int age = 0;

聊聊几种方案的优劣

  • 上文提到, IDE 实际上是在提示我们 age 字段不为 null,你应该对字段进行处理初始化和赋值。换句话说,你要么允许字段为 null,要么你应该就行初始化或赋值而不是使用基本类型的默认值。

方案一禁止使用

  • 方案一相当于忽略这个提醒,那么你的字段在没有赋值的情况下则会使用基本类型的默认值,代码可维护性和可读性都是极差的。

方案二建议使用

  • 方案二是直接使用包装类型的方式,允许字段为 null,当然我们也可以结合 @NonNull 注解来保证字段不为 null
java 复制代码
    @NonNull
    private Integer age;

        // 编译后的 class 文件
        public StudentBuilder name(@NonNull String name) {
            if (name == null) {
                throw new NullPointerException("name is marked non-null but is null");
            } else {
                this.name = name;
                return this;
            }
        }
  • 除此之外,当字段需要默认值时我们还可以结合 @Builder.Default 使用。
java 复制代码
    @Builder.Default
    @NonNull
    private Integer age=3;
    
        // 编译后的 class 文件
        public StudentBuilder age(@NonNull Integer age) {
            if (age == null) {
                throw new NullPointerException("age is marked non-null but is null");
            } else {
                this.age$value = age;
                this.age$set = true;
                return this;
            }
        }

        public Student build() {
            Integer age$value = this.age$value;
            if (!this.age$set) {
                age$value = Student.$default$age();
            }

            return new Student(this.name, age$value);
        }

方案三依据实际情况选择

  • 方案三如果字段不允许为空且有较高性能要求,我们可以考虑选择此方案。
  • 此方案还有一个优势就是避免了 NullPointerException 的处理。
java 复制代码
    @Builder.Default
    private int age=3;
    
        // 编译后的 class 文件 
        public StudentBuilder age(int age) {
            this.age$value = age;
            this.age$set = true;
            return this;
        }

        public Student build() {
            int age$value = this.age$value;
            if (!this.age$set) {
                age$value = Student.$default$age();
            }

            return new Student(this.name, age$value);
        }

方案二 NullPointerException 的处理

  • 当字段不为 null 时使用 @NonNull@Builder.Default 避免此问题。
  • 当字段可为 null 时则需要处理 NullPointerException 问题,下面是一个建议方案:使用 Optional 进行包装,除了可以使用函数式编程的思想使代码更简洁以外,还可以让使用者明确知道该值可能为空,你需要强制进行处理,具备更好的可读性。
java 复制代码
@Builder
@Getter
class Student {
    @NonNull
    private String name;
    private Optional<Integer> age;
}

public class TestMain {

    public static void main(String[] args) {
        Student.StudentBuilder builder = Student.builder().name("Ming");
        builder.age(Optional.of(22));
        Student student = builder.build();
        System.out.println(student.getAge().orElseThrow(() -> new IllegalStateException("Age is not present")));
    }
}

总结

  • 一般情况我们可以直接使用方案二(包装类型) + Optional 的组合,但在一些有性能要求的场景我们可以适当考虑方案三,减少频繁的拆箱和装箱带来的性能损耗以及减少内存占用。
  • 需要注意的是 Optional 不是 Serializable,作为类字段时如果需要序列化则需要进行特殊处理,比如使用 Jackson,它会把空对象看作 null,而有值的对象则把其值看作对应域的值。
java 复制代码
注:实际上 Optional 并不推荐作为类字段,我们可以通过自定义 get 方法的方式返回 Optional 对象,避免引入了额外的序列化工具。

@Builder
@Getter
class Student implements Serializable {
    @NonNull
    private String name;
    private Integer age;

    public Optional<Integer> getAge() {
        return Optional.ofNullable(age);
    }
}

个人简介

👋 你好,我是 Lorin 洛林,一位 Java 后端技术开发者!座右铭:Technology has the power to make the world a better place.

🚀 我对技术的热情是我不断学习和分享的动力。我的博客是一个关于Java生态系统、后端开发和最新技术趋势的地方。

🧠 作为一个 Java 后端技术爱好者,我不仅热衷于探索语言的新特性和技术的深度,还热衷于分享我的见解和最佳实践。我相信知识的分享和社区合作可以帮助我们共同成长。

💡 在我的博客上,你将找到关于Java核心概念、JVM 底层技术、常用框架如Spring和Mybatis 、MySQL等数据库管理、RabbitMQ、Rocketmq等消息中间件、性能优化等内容的深入文章。我也将分享一些编程技巧和解决问题的方法,以帮助你更好地掌握Java编程。

🌐 我鼓励互动和建立社区,因此请留下你的问题、建议或主题请求,让我知道你感兴趣的内容。此外,我将分享最新的互联网和技术资讯,以确保你与技术世界的最新发展保持联系。我期待与你一起在技术之路上前进,一起探讨技术世界的无限可能性。

📖 保持关注我的博客,让我们共同追求技术卓越。

相关推荐
陈平安Java and C4 小时前
MyBatisPlus
java
秋野酱5 小时前
如何在 Spring Boot 中实现自定义属性
java·数据库·spring boot
安的列斯凯奇5 小时前
SpringBoot篇 单元测试 理论篇
spring boot·后端·单元测试
Bunny02125 小时前
SpringMVC笔记
java·redis·笔记
架构文摘JGWZ6 小时前
FastJson很快,有什么用?
后端·学习
BinaryBardC6 小时前
Swift语言的网络编程
开发语言·后端·golang
feng_blog66886 小时前
【docker-1】快速入门docker
java·docker·eureka
邓熙榆6 小时前
Haskell语言的正则表达式
开发语言·后端·golang
枫叶落雨2227 小时前
04JavaWeb——Maven-SpringBootWeb入门
java·maven
m0_748232397 小时前
SpringMVC新版本踩坑[已解决]
java