java中的return this、链式编程和Builder模式

前言

在阅读优秀的开源框架(如 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();

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

  1. maker.addIce() 执行完毕,打印"加冰块"。因为方法里写了 return this,所以它返回了 maker 对象本身
  2. 此时,代码在逻辑上变成了:maker.addSugar()。执行打印"加糖",并再次返回 maker 对象
  3. 代码逻辑又变成了:maker.addMilk()。执行打印"加牛奶",再次返回 maker 对象
  4. 最后执行 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);
    }
}

代码剖析:

  • 前三个方法 setNamesetAgesetCity 都在默默地记录数据,并且统一 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);
// ...

致命缺点

  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 模式代码是如何编写的(区分了必填参数可选参数):

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 类。

传统方式(手动编写):

  1. 私有属性
  2. 全参构造函数
  3. 静态内部类 ProjectBuilder
  4. 内部类中每一个属性的链式赋值方法
  5. 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); 
        }
    }
}

总结

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

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

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

相关推荐
Cosmoshhhyyy1 天前
《Effective Java》解读第49条:检查参数的有效性
java·开发语言
棋子入局1 天前
C语言制作消消乐游戏(2)
c语言·开发语言·游戏
布谷歌1 天前
常见的OOM错误 ( OutOfMemoryError全类型详解)
java·开发语言
WangJunXiang61 天前
GFS分布式文件系统
开发语言·php
民乐团扒谱机1 天前
【微实验】基于matlab的音频提取与信号滤波处理
开发语言·matlab·音视频
eLIN TECE1 天前
springboot和springframework版本依赖关系
java·spring boot·后端
SomeB1oody1 天前
【Python深度学习】3.4. 循环神经网络(RNN)实战:预测股价
开发语言·人工智能·python·rnn·深度学习·机器学习
良木生香1 天前
【C++初阶】:STL——String从入门到应用完全指南(1)
c语言·开发语言·数据结构·c++·算法
老神在在0011 天前
Spring Bean 的六种作用域详解
java·后端·spring
仙草不加料1 天前
互联网大厂Java面试故事实录:三轮场景化技术提问与详细答案解析
java·spring boot·微服务·面试·aigc·电商·内容社区