前言
在阅读优秀的开源框架(如 MyBatis、Lombok 生成的 Builder 类)或现代 Java 代码时,我们经常会看到这种如同流水线一般一气呵成的代码:
Java
// 让人极度舒适的链式调用
Person person = new PersonBuilder()
.setName("张三")
.setAge(18)
.setCity("北京")
.build();
这种 对象.方法().方法().方法() 的写法被称为链式编程(Method Chaining)。它不仅让代码可读性飙升,还极大地减少了代码行数。
而背后实现方式,仅仅是一句简单的:return this;。
一、 没有 return this 的世界
假设我们有一个 CoffeeMaker(咖啡机)类,我们需要加冰、加糖、加奶,传统的写法如下:
Java
// 1. 传统的无返回值(void)写法
public class CoffeeMaker {
public void addIce() { System.out.println("加冰块"); }
public void addSugar() { System.out.println("加糖"); }
public void addMilk() { System.out.println("加牛奶"); }
}
客户端调用时,代码会变成这样:
Java
CoffeeMaker maker = new CoffeeMaker();
maker.addIce(); // 每次都要重新写一遍 maker
maker.addSugar(); // 像个复读机
maker.addMilk(); // 枯燥且乏味
生活比喻:这就像去快餐店点餐,你对服务员说:"我要汉堡。"服务员转身走了。你要加点薯条,还得重新喊:"服务员,再加份薯条!"这显然效率极低。
二、 this到底是什么?
在面向对象编程中,this 关键字代表的就是"当前对象实例本身"(可以理解为中文里的"我")。
如果在方法的最后写上 return this;,并且把方法的返回值类型改成类本身,就意味着:方法执行完毕后,把当前对象自己又扔回给了调用者。
让我们重构刚才的咖啡机:
Java
// 2. 链式编程写法
public class CoffeeMaker {
// 注意:返回值类型变成了 CoffeeMaker 本身
public CoffeeMaker addIce() {
System.out.println("加冰块");
return this; // 👈 核心:干完活,把我(当前对象)交还给你
}
public CoffeeMaker addSugar() {
System.out.println("加糖");
return this;
}
public CoffeeMaker addMilk() {
System.out.println("加牛奶");
return this;
}
public void brew() {
System.out.println("制作完成!");
}
}
// 客户端调用
CoffeeMaker maker = new CoffeeMaker();
maker.addIce().addSugar().addMilk().brew();
三、链式调用是如何流转的?
现在,调用方式如下:
Java
CoffeeMaker maker = new CoffeeMaker();
maker.addIce().addSugar().addMilk().brew();
我们拆解一下这行代码的执行顺序:
maker.addIce()执行完毕,打印"加冰块"。因为方法里写了return this,所以它返回了maker对象本身。- 此时,代码在逻辑上变成了:
maker.addSugar()。执行打印"加糖",并再次返回maker对象。 - 代码逻辑又变成了:
maker.addMilk()。执行打印"加牛奶",再次返回maker对象。 - 最后执行
maker.brew(),打印"制作完成!",因为这个方法是void,链条到此结束。
生活比喻 :你对服务员说:"加冰。"服务员在菜单上打个勾,然后把同一份菜单继续递在你面前 (这就是
return this),微笑着说:"您接着点。"于是你顺理成章地继续点了糖和牛奶。
四、如何实现链式调用
在前言中这段极其舒适的链式调用:
Java
Person person = new PersonBuilder()
.setName("张三")
.setAge(18)
.setCity("北京")
.build();
它底层到底是怎么利用 return this 实现的呢?
这里面其实暗藏了两个关键动作:"链式收集数据" 和 "最终组装"。
Java
// 1. 最终的产品类
public class Person {
private String name;
private int age;
private String city;
// 只有 Builder 才能调用这个构造方法
public Person(String name, int age, String city) {
this.name = name;
this.age = age;
this.city = city;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", city='" + city + "'}";
}
}
// 2. 专属的建造者类
public class PersonBuilder {
// 暂存需要组装的数据
private String name;
private int age;
private String city;
// 动作 1:收集数据,并乖乖地 return this 保持链条不断
public PersonBuilder setName(String name) {
this.name = name;
return this; // 返回当前 Builder 实例
}
public PersonBuilder setAge(int age) {
this.age = age;
return this; // 返回当前 Builder 实例
}
public PersonBuilder setCity(String city) {
this.city = city;
return this; // 返回当前 Builder 实例
}
// 动作 2:完成链式调用,返回真正的目标对象
public Person build() {
// 把暂存的数据拿出来,真正去 new 一个 Person
return new Person(this.name, this.age, this.city);
}
}
代码剖析:
- 前三个方法
setName、setAge、setCity都在默默地记录数据,并且统一return this。 - 就像我们在网购时,不断地把商品加入购物车,但还没有结账,购物车(Builder)一直都在。
- 最后一个
build()方法没有return this,而是返回了真正的Person对象,这就像点击了"提交订单",返回对象。
五、进阶:JavaBeans中的Builder 模式
5.1 设计思想
在没有建造者模式之前,当我们遇到一个属性特别多、且很多属性是可选的复杂对象时,通常会面临两种极其痛苦的写法(反模式):
5.1.1 折叠构造函数噩梦
为了适配不同的参数组合,我们会被迫写无数个重载的构造函数:
Java
public User(String name) {...}
public User(String name, int age) {...}
public User(String name, int age, String phone) {...}
致命缺点 :当参数达到 5 个以上时,调用者根本记不住顺序。比如 new User("Tom", 18, null, null, "Beijing"),满屏的 null,极易传错参数。
5.1.2 JavaBeans 模式(疯狂 Set)
先用无参构造创建对象,然后挨个调用 set 方法:
Java
User user = new User();
user.setName("Tom");
user.setAge(18);
// ...
致命缺点:
- 破坏了不可变性(Immutability) :对象一旦创建,随时都能被
set修改,在多线程环境下极不安全。 - 状态不一致 :在对象
new出来到全部set完的过程中,这个对象处于一个"半残疾"的无效状态,如果此时被其他方法拿去用,直接报错。
5.1.3 Builder 的核心设计思想
Builder 模式的设计哲学就是:将一个复杂对象的"构建过程"与它的"最终表示"分离。
它就像是雇佣了一个专属的装配工人(Builder)。
你把需求(各种参数)一点点告诉工人,工人帮你记在一个小本子上。直到你最后喊一句"完工(build)!",工人就会瞬间把一个完整且不可篡改的最终产品交给你。
5.2 核心原理机制
一个标准的现代版 Builder 模式通常具有以下几个特征:
- 目标类的构造方法私有化(Private) :防止外部直接
new,逼迫调用者必须通过 Builder 来创建。 - 静态内部类(Static Inner Class) :在目标类内部创建一个静态的
XxxBuilder类。这个 Builder 类拥有和目标类一模一样的属性。 - 链式调用(Method Chaining) :Builder 中的所有设值方法(如
setAge)都会返回this(即 Builder 自己)。 - 终结技
build()方法 :在 Builder 中提供一个build()方法。调用它时,它会去调用目标类的私有构造函数,把暂存的数据全部塞进去,返回真正的目标对象。
5.3 实战:User 实体
我们来看一个标准的、工业级的 Builder 模式代码是如何编写的(区分了必填参数 和可选参数):
Java
public class User {
// 1. 目标对象的属性(通常加上 final,保证一旦创建绝对不可变)
private final String name; // 必填
private final String idCard; // 必填
private final int age; // 可选
private final String address; // 可选
// 2. 私有化构造方法,只接收自己的 Builder 作为参数
private User(UserBuilder builder) {
this.name = builder.name;
this.idCard = builder.idCard;
this.age = builder.age;
this.address = builder.address;
}
// 省略 Getter 方法...
@Override
public String toString() {
return "User{name='" + name + "', idCard='" + idCard + "', age=" + age + ", address='" + address + "'}";
}
// ----------------------------------------------------
// 3. 静态内部类:建造者 (相当于装配工人)
// ----------------------------------------------------
public static class UserBuilder {
// 与外部类一模一样的属性暂存区
private final String name; // 必填项,用 final
private final String idCard; // 必填项,用 final
private int age; // 可选
private String address; // 可选
// 4. 必填参数,强制通过 Builder 的构造方法传入
public UserBuilder(String name, String idCard) {
this.name = name;
this.idCard = idCard;
}
// 5. 可选参数,使用链式调用 (return this)
public UserBuilder age(int age) {
this.age = age;
return this;
}
public UserBuilder address(String address) {
this.address = address;
return this;
}
// 6. 核心动作:校验并构建最终对象
public User build() {
// 可以在这里做极其严格的参数校验!
if (this.age < 0 || this.age > 150) {
throw new IllegalArgumentException("年龄不合法");
}
// 校验通过,召唤真正的产品
return new User(this);
}
}
}
客户端使用体验:
Java
public class Client {
public static void main(String[] args) {
// 必填参数在创建 Builder 时强约束,可选参数通过链式丝滑配置
User vipUser = new User.UserBuilder("张三", "110105xxxxxx")
.age(28)
.address("北京市朝阳区")
.build();
System.out.println(vipUser);
}
}
5.4 优缺点总结
- 优点 :
- 代码极其易读,告别了一长串不知所云的构造参数。
- 安全:目标对象可以被设计成真正的不可变对象(Immutable),天生线程安全。
- 对象的初始化逻辑和业务逻辑彻底解耦,可以在
build()方法里做统一的参数合法性校验。
- 缺点 :
- 代码冗长:为了创建一个类,要额外多写一个几乎拥有一样属性的 Builder 类,增加了工作量。
在Java开发中,为了解决"手写 Builder 代码太长"这个唯一缺点,我们通常会使用 Lombok ,只需要在实体类上加一个 @Builder 注解,就能让编译器在底层自动帮你生成上面那一大坨代码。
5.5 Lombok的@Builder注解
在 Java 开发中,Lombok 几乎是项目的标配。它通过在编译期动态注入代码,减少重复体力的代码。
使用前提:在 pom.xml 中加入 Lombok 依赖。
想象我们要创建一个 Project 类。
传统方式(手动编写):
- 私有属性
- 全参构造函数
- 静态内部类
ProjectBuilder - 内部类中每一个属性的链式赋值方法
build()方法
代码量通常在 50-100 行左右。
Lombok 方式:
Java
import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class Project {
private String name;
private String leader;
private int budget;
private List<String> tags;
}
背后发生了什么?
当按下编译键(或 IDE 自动编译)时,Lombok 会自动在字节码中生成那个复杂的内部 Builder 类。对于调用者来说,用法完全一样:
java
Project.builder().name("AI系统").leader("张三").build();
相当于代码:
java
public class Project {
private String name;
private String leader;
// ... 其他属性
// Lombok 偷偷加的:包级私有的全参构造
Project(String name, String leader) {
this.name = name;
this.leader = leader;
}
// 步骤1对应的静态方法
public static ProjectBuilder builder() {
return new ProjectBuilder();
}
// Lombok 偷偷生成的装配工内部类
public static class ProjectBuilder {
private String name;
private String leader;
// 没有写构造函数,那么系统默认是无参构造
// 步骤2对应的设值方法 (经典的 return this)
public ProjectBuilder name(String name) {
this.name = name;
return this;
}
// 步骤3对应的设值方法 (经典的 return this)
public ProjectBuilder leader(String leader) {
this.leader = leader;
return this;
}
// 步骤4对应的终结方法
public Project build() {
// 把自己暂存的数据,拿去 new 真正的对象
return new Project(this.name, this.leader);
}
}
}
总结
-
链式调用的核心底层机制在于方法末尾返回当前对象实例(
return this)。 -
基于此机制演化的 Builder(建造者)模式 ,有效解决了复杂对象构建时构造参数过多、易传错,以及频繁调用
set方法导致的代码冗余与线程不安全问题。 -
针对手动编写 Builder 类造成的代码膨胀缺陷,现代 Java 工程实践中通常引入 Lombok 库,通过
@Builder注解在编译期自动完成底层构建逻辑的生成。