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
| 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.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
| public class 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();
|
我们拆解一下这行代码的执行顺序:
maker.addIce() 执行完毕,打印“加冰块”。因为方法里写了 return this,所以它返回了 maker 对象本身。
- 此时,代码在逻辑上变成了:
maker.addSugar()。执行打印“加糖”,并再次返回 maker 对象。
- 代码逻辑又变成了:
maker.addMilk()。执行打印“加牛奶”,再次返回 maker 对象。
- 最后执行
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
| public class Person { private String name; private int age; private String city;
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 + "'}"; } }
public class PersonBuilder { private String name; private int age; private String city;
public PersonBuilder setName(String name) { this.name = name; return this; }
public PersonBuilder setAge(int age) { this.age = age; return this; }
public PersonBuilder setCity(String city) { this.city = city; return this; }
public Person build() { 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 折叠构造函数噩梦
为了适配不同的参数组合,我们会被迫写无数个重载的构造函数:
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);
|
致命缺点:
- 破坏了不可变性(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 模式代码是如何编写的(区分了必填参数和可选参数):
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 { private final String name; private final String idCard; private final int age; private final String address;
private User(UserBuilder builder) { this.name = builder.name; this.idCard = builder.idCard; this.age = builder.age; this.address = builder.address; }
@Override public String toString() { return "User{name='" + name + "', idCard='" + idCard + "', age=" + age + ", address='" + address + "'}"; }
public static class UserBuilder { private final String name; private final String idCard; private int age; private String address;
public UserBuilder(String name, String idCard) { this.name = name; this.idCard = idCard; }
public UserBuilder age(int age) { this.age = age; return this; }
public UserBuilder address(String address) { this.address = address; return this; }
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) { 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 方式:
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; Project(String name, String leader) { this.name = name; this.leader = leader; }
public static ProjectBuilder builder() { return new ProjectBuilder(); }
public static class ProjectBuilder { private String name; private String leader;
public ProjectBuilder name(String name) { this.name = name; return this; }
public ProjectBuilder leader(String leader) { this.leader = leader; return this; }
public Project build() { return new Project(this.name, this.leader); } } }
|
总结
链式调用的核心底层机制在于方法末尾返回当前对象实例(return this)。
基于此机制演化的 Builder(建造者)模式,有效解决了复杂对象构建时构造参数过多、易传错,以及频繁调用 set 方法导致的代码冗余与线程不安全问题。
针对手动编写 Builder 类造成的代码膨胀缺陷,现代 Java 工程实践中通常引入 Lombok 库,通过 @Builder 注解在编译期自动完成底层构建逻辑的生成。