版本
- JDK 8
起因
- 最近合并代码中发现了一个有意思的报错:
Lombok builder is missing non nullable fields
,大意是说 Lombok 构建器缺少对非空字段的处理,下面是对情况复现 demo:
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
层面的提示而编译器并不会实际进行检查,程序是可以正常运行的。
方案二
- 设置
age
为Integer
,表示允许字段为null
。
private Integer age;
方案三
Builder.Default
设置默认值
@Builder.Default
private int age = 0;
聊聊几种方案的优劣
- 上文提到,
IDE
实际上是在提示我们age
字段不为null
,你应该对字段进行处理初始化和赋值。换句话说,你要么允许字段为null
,要么你应该就行初始化或赋值而不是使用基本类型的默认值。
方案一禁止使用
- 方案一相当于忽略这个提醒,那么你的字段在没有赋值的情况下则会使用基本类型的默认值,代码可维护性和可读性都是极差的。
方案二建议使用
- 方案二是直接使用包装类型的方式,允许字段为
null
,当然我们也可以结合@NonNull
注解来保证字段不为null
。
@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
使用。
@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
的处理。
@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
进行包装,除了可以使用函数式编程的思想使代码更简洁以外,还可以让使用者明确知道该值可能为空,你需要强制进行处理,具备更好的可读性。
@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,而有值的对象则把其值看作对应域的值。
个人简介