java中的return this和链式编程

前言

在阅读优秀的开源框架(如 MyBatis、Lombok 生成的 Builder 类)或现代 Java 代码时,我们经常会看到这种如同流水线一般一气呵成的代码:

1
2
3
4
5
6
// 让人极度舒适的链式调用
Person person = new PersonBuilder()
.setName("张三")
.setAge(18)
.setCity("北京")
.build();

这种 对象.方法().方法().方法() 的写法被称为链式编程(Method Chaining)。它不仅让代码可读性飙升,还极大地减少了代码行数。

而背后实现方式,仅仅是一句简单的:return this;

一、 没有 return this 的世界

假设我们有一个 CoffeeMaker(咖啡机)类,我们需要加冰、加糖、加奶,传统的写法如下:

1
2
3
4
5
6
// 1. 传统的无返回值(void)写法
public class CoffeeMaker {
public void addIce() { System.out.println("加冰块"); }
public void addSugar() { System.out.println("加糖"); }
public void addMilk() { System.out.println("加牛奶"); }
}

客户端调用时,代码会变成这样:

1
2
3
4
CoffeeMaker maker = new CoffeeMaker();
maker.addIce(); // 每次都要重新写一遍 maker
maker.addSugar(); // 像个复读机
maker.addMilk(); // 枯燥且乏味

生活比喻:这就像去快餐店点餐,你对服务员说:“我要汉堡。”服务员转身走了。你要加点薯条,还得重新喊:“服务员,再加份薯条!”这显然效率极低。


二、 this到底是什么?

在面向对象编程中,this 关键字代表的就是“当前对象实例本身”(可以理解为中文里的“我”)。

如果在方法的最后写上 return this;,并且把方法的返回值类型改成类本身,就意味着:方法执行完毕后,把当前对象自己又扔回给了调用者。

让我们重构刚才的咖啡机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 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();

三、链式调用是如何流转的?

现在,调用方式如下:

1
2
CoffeeMaker maker = new CoffeeMaker();
maker.addIce().addSugar().addMilk().brew();

我们拆解一下这行代码的执行顺序:

  1. maker.addIce() 执行完毕,打印“加冰块”。因为方法里写了 return this,所以它返回了 maker 对象本身
  2. 此时,代码在逻辑上变成了:maker.addSugar()。执行打印“加糖”,并再次返回 maker 对象
  3. 代码逻辑又变成了:maker.addMilk()。执行打印“加牛奶”,再次返回 maker 对象
  4. 最后执行 maker.brew(),打印“制作完成!”,因为这个方法是 void,链条到此结束。

生活比喻:你对服务员说:“加冰。”服务员在菜单上打个勾,然后把同一份菜单继续递在你面前(这就是 return this),微笑着说:“您接着点。”于是你顺理成章地继续点了糖和牛奶。


四、如何实现链式调用

在前言中这段极其舒适的链式调用:

1
2
3
4
5
Person person = new PersonBuilder()
.setName("张三")
.setAge(18)
.setCity("北京")
.build();

它底层到底是怎么利用 return this 实现的呢?

这里面其实暗藏了两个关键动作:“链式收集数据”“最终组装”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 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);
}
}

代码剖析:

  • 前三个方法 setNamesetAgesetCity 都在默默地记录数据,并且统一 return this
  • 就像我们在网购时,不断地把商品加入购物车,但还没有结账,购物车(Builder)一直都在。
  • 最后一个 build() 方法没有 return this,而是返回了真正的 Person 对象,这就像点击了“提交订单”,返回对象。

五、进阶:JavaBeans中的Builder 模式

5.1 设计思想

在没有建造者模式之前,当我们遇到一个属性特别多、且很多属性是可选的复杂对象时,通常会面临两种极其痛苦的写法(反模式):

5.1.1 折叠构造函数噩梦

为了适配不同的参数组合,我们会被迫写无数个重载的构造函数:

1
2
3
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 方法:

1
2
3
4
User user = new User();
user.setName("Tom");
user.setAge(18);
// ...

致命缺点

  1. 破坏了不可变性(Immutability):对象一旦创建,随时都能被 set 修改,在多线程环境下极不安全。
  2. 状态不一致:在对象 new 出来到全部 set 完的过程中,这个对象处于一个“半残疾”的无效状态,如果此时被其他方法拿去用,直接报错。

5.1.3 Builder 的核心设计思想

Builder 模式的设计哲学就是:将一个复杂对象的“构建过程”与它的“最终表示”分离。

它就像是雇佣了一个专属的装配工人(Builder)

你把需求(各种参数)一点点告诉工人,工人帮你记在一个小本子上。直到你最后喊一句“完工(build)!”,工人就会瞬间把一个完整且不可篡改的最终产品交给你。

5.2 核心原理机制

一个标准的现代版 Builder 模式通常具有以下几个特征:

  1. 目标类的构造方法私有化(Private):防止外部直接 new,逼迫调用者必须通过 Builder 来创建。
  2. 静态内部类(Static Inner Class):在目标类内部创建一个静态的 XxxBuilder 类。这个 Builder 类拥有和目标类一模一样的属性。
  3. 链式调用(Method Chaining):Builder 中的所有设值方法(如 setAge)都会返回 this(即 Builder 自己)。
  4. 终结技 build() 方法:在 Builder 中提供一个 build() 方法。调用它时,它会去调用目标类的私有构造函数,把暂存的数据全部塞进去,返回真正的目标对象。

5.3 实战:User 实体

我们来看一个标准的、工业级的 Builder 模式代码是如何编写的(区分了必填参数可选参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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);
}
}
}

客户端使用体验:

1
2
3
4
5
6
7
8
9
10
11
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 类。

传统方式(手动编写):

  1. 私有属性
  2. 全参构造函数
  3. 静态内部类 ProjectBuilder
  4. 内部类中每一个属性的链式赋值方法
  5. build() 方法

代码量通常在 50-100 行左右。

Lombok 方式:

1
2
3
4
5
6
7
8
9
10
11
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 类。对于调用者来说,用法完全一样:

1
Project.builder().name("AI系统").leader("张三").build();

相当于代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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);
}
}
}

总结

  1. 链式调用的核心底层机制在于方法末尾返回当前对象实例(return this

  2. 基于此机制演化的 Builder(建造者)模式,有效解决了复杂对象构建时构造参数过多、易传错,以及频繁调用 set 方法导致的代码冗余与线程不安全问题。

  3. 针对手动编写 Builder 类造成的代码膨胀缺陷,现代 Java 工程实践中通常引入 Lombok 库,通过 @Builder 注解在编译期自动完成底层构建逻辑的生成。