使用 new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象是两种主要的方式。
简单直接的结论是:对于参数较多(超过3-4个)或包含可选参数的复杂对象,.builder() 更好;对于简单对象,new 更好。
1. 直观对比
假设有一个 User 类,包含 5 个属性:firstName, lastName, age, phone, address。
使用 new (构造函数)
sql
<JAVA>
// 如果有些参数可选,你可能不得不传 null
User user = new User("Zhang", "San", 25, null, "Beijing");
问题:
- 可读性差 :如果不看 IDE 提示或源码,你很难知道
25是年龄还是 ID?"Beijing"是地址还是备注? - 参数顺序锁死:必须严格按照构造函数定义的顺序传参,一旦记错顺序(比如两个 String 类型的参数),编译不报错但运行逻辑错误。
- 要么是创建一个空对象,再去set属性值,看起来臃肿
使用 .builder() (建造者模式)
scss
<JAVA>
User user = User.builder()
.firstName("Zhang")
.lastName("San")
.age(25)
.address("Beijing")
.build();
优势:
- 代码如文档 :
.age(25)一目了然。 - 灵活 :没有传入的参数(如
phone)会自动赋默认值或 null,不需要显式传递null。 - 顺序无关 :可以先调
.address()再调.firstName()。
2. 深度优缺点分析
🅰️ new (Constructor)
优点:
- 原生支持:Java 语言内置机制,无需额外代码或注解。
- 强制性:如果某个对象必须依赖某些参数才能初始化,构造函数可以强制要求调用者传入这些参数(编译期检查)。
- 性能极高 :没有中间对象的创建开销(虽然 Builder 的开销微乎其微,但在极端高性能场景下
new更快)。 - 代码量少:对于只有 1-2 个字段的简单类(如 DTO、VO),写起来最快。
缺点:
- 伸缩构造函数反模式 (Telescoping Constructor Problem) :当参数变多,或者有多种参数组合时,你不得不写一大堆构造函数(只有A的,有A和B的,有A、B和C的...)。
- 维护困难:如果在中间插入一个新的字段,所有调用该构造函数的地方都会报错,或者需要重构。
🅱️ .builder() (Builder Pattern)
通常配合 Lombok 的 @Builder 注解使用。
优点:
- 可读性极强:链式调用(Fluent API),代码清晰。
- 处理可选参数:不需要为不同的参数组合编写多个构造函数。
- 支持不可变对象 (Immutability) :通常 Builder 最终构建出的对象没有 Setter 方法,这对于多线程安全和数据一致性非常有好处。对象一旦
build()出来就是最终状态。 - 易于维护:新增字段时,只需要在链式调用中增加一行,不影响其他不需要该字段的代码。
缺点:
- 样板代码多:如果不使用 Lombok,手动手写 Builder 模式需要写大量的静态内部类代码。
- 对象开销:创建目标对象前,会先创建一个临时的 Builder 对象(虽然现代 JVM 对此优化很好,GC 回收也很快,但理论上内存占用稍多)。
- 复杂性:对于只有 1 个字段的类,用 Builder 显得杀鸡用牛刀。
3. 哪个更好?
场景一:使用 .builder() (推荐)
- 参数数量 > 4 个:为了可读性,强烈建议使用。
- 参数具有多种组合:例如搜索条件对象(可能有名字、可能有日期、可能有状态,组合无穷无尽)。
- 需要不可变对象:你希望对象创建后不被修改(没有 Setter),Builder 是最佳伴侣。
- 字段类型重复多 :例如有 5 个
String类型的字段,用构造函数极其容易传错位置。
场景二:使用 new
- 参数数量 < 3 个 :例如
new Point(x, y),比Point.builder().x(1).y(2).build()简洁得多。 - 必须强制参数 :如果你要求对象创建时必须包含
id和name,使用构造函数new Obj(id, name)可以利用编译器强制约束,而 Builder 默认是允许不调用的(除非在build()方法里加校验逻辑)。 - 极度在意性能:在超高频交易或底层库开发中,为了省去 Builder 对象的创建开销。
4. 最佳实践 (Modern Java)
在现代 Java 开发(尤其是 Spring Boot 项目)中,最流行的做法是 结合 Lombok:
typescript
<JAVA>
import lombok.Builder;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
private String address;
}
用法:
- 简单场景或框架反射用:
new User() - 复制属性场景:
new User(name, age, address) - 业务逻辑构建场景:
User.builder().name("X").build()
总结: 尽量默认使用 @Builder,因为它提供了最好的可读性和维护性,除非你的类非常简单(如 Pair, Point 这种值对象)
5. 实体类可以通过@Data 和@Builder 两个注解,创建对象和set值,为什么说build() 出来就是最终状态?
Builder 模式本身是为了解决复杂对象的创建问题,而"不可变性"是 Builder 模式最常搭配的"最佳拍档",但并非"强制绑定"。 Lombok 的 @Builder 只是一个工具,它允许你"离经叛道"地同时使用 @Setter。
为什么说"Builder 天生适合不可变对象"?
想象一下,如果你想设计一个绝对不可变 (Immutable)的类 User(即:多线程安全,创建后不可改):
- 你必须把所有字段设为
final。 - 你必须删掉所有 Setter 方法。
这时候问题来了:你怎么给它赋值?
- 方法 A:用 Setter? 不行,你刚把它删了。
- 方法 B:用构造函数? 如果字段有 10 个,你就陷入了刚才说的"伸缩构造函数反模式",代码极其难看。
方法 C:用 Builder! 这是唯一的完美解法。Builder 充当了一个缓冲容器,先在 Builder 里面把参数一点点凑齐,最后调用 .build() 的瞬间,一次性把所有参数传给那个私有的全参构造函数。
结论 :之所以说 Builder 支持不可变对象,是因为如果没有 Builder,创建复杂的不可变对象极其痛苦。Builder 是实现不可变对象的"最佳助攻"。
那为什么 Lombok 允许 @Builder 和 @Data (Setter) 共存?
这是 Lombok 为了实用性(Pragmatism) 做出的妥协。
在 Java 的真实开发世界中,有两种截然不同的场景:
场景 A:领域模型 (Domain Object) / 值对象 (VO)
- 要求:数据一致性、多线程安全。
- 写法 :
@Builder+@Value(或者@Getter,无 Setter)。 - 状态 :此时 Builder 构建出来的对象是真的不可变。
- 这是教科书式的标准用法。
场景 B:实体类 (JPA Entity) / 数据传输对象 (DTO)
- 要求:需要被框架反射修改、需要后续业务逻辑更新字段。
- 写法 :
@Builder+@Data(包含 Setter)。 - 状态 :此时 Builder 仅仅是作为一个 "优雅的构造器" 存在,对象本身是可变的。
为什么这么做? 有时候我们仅仅是讨厌写 new User(param1, param2...) 这种长代码,觉得 .builder().name("A").build() 写起来更可读、更优雅。但同时,后续业务逻辑里我又确实需要 user.setStatus(1)。 Lombok 作为一个工具库,它把选择权交给了你:你想用来做防弹衣(不可变),还是做方便面(仅为了好用),都可以。
例:
🔴 真正的不可变模式
这是 Builder 被设计出来的初衷。
java
<JAVA>
@Builder
@Getter // 只有 Get,没有 Set
// 或者直接用 @Value,它会自动把字段设为 private final
public class ImmutableUser {
private final String name;
private final int age;
}
// ImmutableUser user = ImmutableUser.builder().name("A").build();
// user.setName("B"); // ❌ 编译报错!根本没有这个方法!
// 安全!
🟡 混合模式 (Lombok 的灵活变通)
这是为了写代码爽,但牺牲了不可变性。
java
<JAVA>
@Builder
@Data // 包含了 @Setter
public class MutableUser {
private String name;
private int age;
}
// MutableUser user = MutableUser.builder().name("A").build();
// user.setName("B"); // ✅ 可以修改
// 不安全,但在 CRUD 业务中很常见。
6. @Builder原理
@Builder 注解本身在运行时(Runtime)不会创建任何对象,也不会执行 new 操作。
当你编译代码时,Lombok 插件会拦截编译过程 ,读取你的类定义,然后自动生成大量的 Java 源代码(包括内部静态 Builder 类、构造函数调用等),并将这些生成的代码插入到编译流程中。
你看到的 .class 文件里之所以有 new 操作,是因为 Lombok 在编译阶段已经把包含 new 的代码写好并编译进去了,而不是在运行时动态创建的。
@Builder 是如何工作的?(编译时魔法)
Lombok 是一个 注解处理器(Annotation Processor) 。它的工作流程如下:
- 源码阶段 :你写了一个简单的类,加上了
@Builder。
arduino
@Builder
public class User {
private String name;
private int age;
}
此时,内存里还没有 Builder 类,也没有 new User(...) 的代码。
-
编译阶段(关键步骤) :
javac编译器启动。- Lombok 的注解处理器被触发。
- Lombok 扫描到
@Builder。 - Lombok 动态生成代码 :它在内存中构造出一个完整的内部静态类
UserBuilder,并生成了builder()方法、name()方法、age()方法、build()方法等。 - 注入
new操作 :在生成的build()方法中,Lombok 硬编码 了new User(this.name, this.age)这样的语句。
-
生成的代码长什么样?
Lombok 实际上把你的类变成了下面这个样子(这是编译器真正处理的内容):
arduino
public class User {
private String name;
private int age;
// 1. 私有构造函数(防止外部直接 new User,强制走 Builder)
private User(String name, int age) {
this.name = name;
this.age = age;
}
// 2. 静态方法,返回 Builder 实例
public static UserBuilder builder() {
return new UserBuilder(); // 这里 new 了 Builder 对象
}
// 3. 生成的内部静态 Builder 类
public static class UserBuilder {
private String name;
private int age;
// 链式调用方法
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// 4. 关键的 build 方法:这里发生了对象的最终创建
public User build() {
// 【重点】:这里的 new User(...) 是 Lombok 在编译时生成的代码!
return new User(this.name, this.age);
}
}
}
- 字节码阶段 :编译器将上述包含
new操作 的完整代码编译成.class文件。 - 运行阶段 :JVM 加载这个已经包含完整逻辑的
.class文件。当你调用User.builder().name("A").build()时,只是正常执行已经存在的字节码指令(INVOKESPECIAL即new指令)。
什么时候用到 new ?
在 @Builder 的模式下,new 关键字出现在两个地方,但都是编译后存在的:
A. 创建 Builder 对象时
当你调用 User.builder() 时:
- 底层执行的是:
return new UserBuilder(); - 时机:编译时由 Lombok 生成该语句,运行时执行该语句创建 Builder 实例。
B. 创建目标对象时(最关键)
当你调用 .build() 时:
- 底层执行的是:
return new User(this.name, this.age); - 时机 :编译时由 Lombok 生成该语句,运行时执行该语句创建最终的
User对象。 - 注意 :Lombok 通常会将原类的构造函数权限改为
private,所以除了build()方法内部,其他地方无法直接使用new User()。这就是 Builder 模式强制规范对象创建的方式。
为什么你看不到源码里的 new ?
因为你看到的是原始源码(Source Code) 。
- 原始源码 :只有注解
@Builder。 - 编译后的源码(反编译 view) :包含了 Lombok 生成的所有类和
new语句。
你在 IntelliJ IDEA 中如果安装了 Lombok 插件,IDE 会"欺骗"你,让你在源码视图也能看到生成的方法(虚线显示),但这只是为了方便你开发(自动补全、跳转)。真正的物理代码是在编译那一刻才"变"出来的。
可以尝试以下操作来验证:
- 编写一个带
@Builder的类。 - 编译项目 (
Build -> Rebuild Project)。 - 在 IDEA 中打开对应的
.class文件(通常在out/production/...或target/classes/...目录下,或者按Ctrl+Shift+N查找类名后选择.class后缀的文件)。 - 使用反编译插件(如 FernFlower,IDEA 自带)查看。
- 你会清晰地看到
private User(...)构造函数和build()方法里的new User(...)。
机制 :@Builder 是编译时代码生成技术 ,不是运行时反射或动态代理。new 的位置 :new 操作符是由 Lombok 在编译阶段 自动写入到生成的 build() 方法和 builder() 方法中的。当你在运行时调用 .build() 时,JVM 执行的是那段早已生成好 的 new 指令。
- 优势 :因为是编译期生成,所以没有运行时性能损耗(不像反射那样慢),且类型安全。