作者 | 李增光
杏仁后端工程师。 只有变秃,才能变强!
记得之前,我在《effective java》上看过 Builder 构建器相关的内容,但实际开发中不经常用。后来,在项目中使用了 lombok ,发现它有一个注解 @Builder
,就是为 java bean 生成一个构建器。于是,回头重新复习了下相关知识,整理如下。
Builder 模式 又被称作 建造者模式 或者 生成器模式 .是一种设计模式。
维基百科上的定义为:
将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。
一个典型的 Builder 模式的 UML 类图如下:
角色介绍:
Product 产品类 : 产品的抽象类。
Builder : 抽象类, 规范产品的组建,一般是由子类实现具体的组件过程。
ConcreteBuilder : 具体的构建器。
Director : 统一组装过程(可省略)。
注意与抽象工厂模式的区别: 抽象工厂模式与生成器相似,因为它也可以创建复杂对象.主要的区别是生成器模式着重于一步步构造一个复杂对象.而抽象工厂模式着重于多个系列的产品对象(简单的或是复杂的).生成器在最后的一步返回产品,而对于抽象工厂来说,产品是立即返回的。
若一个类具有大量的成员变量,我们就需要提供一个全参的构造器或大量的 set 方法.这让实例的创建和赋值,变得很麻烦,且不直观.我们通过构建器,可以让变量的赋值变成链式调用,而且调用的方法名对应着成员变量的名称.让对象的创建和赋值都变得很简洁、直观。
Builder 模式的使用场景:
相同的方法,不同的执行顺序,产生不同的事件结果时。
多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时。
产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适。
当初始化一个对象特别复杂,如参数多,且很多参数都具有默认值时。
借助于 Lombok 我们可以快速创建 Builder 模式。首先,创建一个名为 User 的 Java Bean,非常简单,只有两个属性,sex,和 name。其中 @Builder
可以自动为 User 对象生成一个构建器, @ToString
可以自动为 User 对象生成 toStrng()
方法。
@Builder @ToString public class User { private Integer sex; private String name; }
现来测试一下 Lombok 为我们自动提供的构建器功能:
@Test public void builderTest() { User user = User.builder() .name("杏仁") .sex(1) .build(); System.out.println(user.toString()); }
会看到控制台打印:
User(sex=1,name=杏仁)
可以看到,打印结果就是我们想要的样子,但是 Lombok 为我们做了什么事情呢?
使用 Lombok 需要注意理解 Lombok 的注解做了什么,否则很容易出错。
*注意:下面请区分两组名词:"builder方法"和“build方法”,“构造器”和“构建器”。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.xingren.lomboklearn.pojo; public class User { private Integer sex; private String name; User(final Integer sex,final String name) { this.sex = sex; this.name = name; } public static User.UserBuilder builder() { return new User.UserBuilder(); } public String toString() { return "User(sex=" + this.sex + ",name=" + this.name + ")"; } public static class UserBuilder { private Integer sex; private String name; UserBuilder() { } public User.UserBuilder sex(final Integer sex) { this.sex = sex; return this; } public User.UserBuilder name(final String name) { this.name = name; return this; } public User build() { return new User(this.sex,this.name); } public String toString() { return "User.UserBuilder(sex=" + this.sex + ",name=" + this.name + ")"; } } }
我们通过反编译 User.class,获得上方的源码(最好用 idea 自带的反编译器,jd-gui 反编译的源码不全)。
我们发现源码中有一个 UserBuilder
的静态内部类,我们在调用 builder 方法时,实际返回了这个静态内部类的实例。
这个 UserBuilder
类,具有和User相同的成员变量,且拥有名为 sex
, name
的方法.这些以 User 的成员变量命名的方法,都是给 UserBuilder
的成员变量赋值,并返回 this 。
这些方法返回 this,其实就是返回调用这些方法的 UserBuilder
对象,也可称为“返回对象本身”。通过返回对象本身,形成了方法的链式调用。
再看 build 方法,它是 UserBuilder
类的方法.它创建了一个新的 User 对象,并将自身的成员变量值,传给了User 的成员变量。
所以为什么这种模式叫''构建器'',因为要创建 User 类的实例,首先要创建内部类 UserBuilder
。而这个UserBuilder 也就是构建器,是创建 User 对象的一个过渡者. 所以利用这种模式,会有中间实例的创建,会加大虚拟机内存的消耗。
不一定要用到构建器模式,通过上面分析 Lombok 的构建器原理可知,Lombok 的 @Builder
更适合 final 修饰的成员变量。
因为 final 修饰的成员变量,需要在实例创建时就把值确定下来。(常见的如: web 开发中的 Query/Form 等查询入参,变量通常会设置为 final 的) 在类具有大量成员变量的时候,我们是不希望用户直接调用全参构造器的。
那如果有大量属性,但不需要它是成员变量不可变的对象,我们还需要构建器模式吗?个人认为,不需要,我们可以参考构建器,把代码赋值改成链式的即可:
public class User { private Integer sex; private String name; public static User build() { return new User(); } private User() { } public User sex(Integer sex) { this.sex = sex; return this; } public User name(String name){ this.name = name; return this; } }
我们要做的很简单: 私有化构造函数,在 build()
方法中实例化 User 对象。
我们先来改造一下 User 类:
@Builder @ToString public class User { @NonNull private Integer sex = 1; private String name; }
添加一个 @NoNull
注解,此注解会要求sex属性不能为空。而我们又为 sex 属性设置了默认值 1 ,看起来应该没什么问题,我们再来测试一下:
@Test public void builderTest() { User user = User.builder() .name("杏仁") .build(); System.out.println(user.toString()); }
我们去掉了sex属性的设置,因为它已经设置了默认值,但是运行就会发现报如下错误:
java.lang.NullPointerException: sex is marked @NonNull but is null
这就很奇怪了,我们明明给 sex 属性设置了默认值啊,怎么还会是null呢?
为了一探究竟,我们还是反编译User.class文件看下Lombok 到底做了什么?以下为简化代码:
public class User { @NonNull private Integer sex = 1; private String name; // ...(代码略) public static class UserBuilder { private Integer sex; private String name; // ...(代码略) public User build() { return new User(this.sex,this.name); } // ...(代码略) } }
通过反编译后的代码可以看到,User 的 sex 属性已经有了默认值为 1。 但是 内部类 UserBuilder 却没有默认值,我们再调用 User.builder() 方法是,实例化的是 UserBuilder 内部类。
最后调用 UserBuilder.build() 方法时,是把 UserBuilder 类的属性传给了 User 类,导致 User 类的默认值被 UserBuilder 类覆盖。
而在User的全参构造方法中,则会判断 sex 是否 null ,是的话就会抛出 NullPointerException("sex is marked @NonNull but is null")
异常。
如何解决呢?
解决起来也很简单,不止一种方法,其中一种是 我们可以在需要默认值的属性上使用 @Builder.Default
注解
@NonNull @Builder.Default private Integer sex = 1;
再次测试就会发现,输出结果如我们期望那样。
那这个 @Builder.Default
注解到底做了什么事情呢? 老规矩,还是反编译下 User.class,查看下简化源代码:
public class User { @NonNull private Integer sex; private String name; private static Integer $default$sex() { return 1; } // ... public static User.UserBuilder builder() { return new User.UserBuilder(); } public static class UserBuilder { private boolean sex$set; private Integer sex; private String name; // ... public User build() { Integer sex = this.sex; if (!this.sex$set) { sex = User.$default$sex(); } return new User(sex,this.name); } // ... } }
首先我们发现,User 类多了一个静态方法: $default$sex()
,此方法返回了 sex 的默认值。
接下来看内部类 UserBuilder 多了一个 boolean 类型的变量 sex$set
。
结合 sex() 方法来推断: 可知,在调用构建器设置 sex 属性时,会先判断是否为空(因为有 @NoNull
注解),并且如果不为空,会设置内部类 UserBuilder 的 sex 属性值,并且把 sex$set
置为 true.由此我们可知, sex$set
变量就是来表示 sex是否是默认值的。
最后,在 UserBuilder.build() 方法里面不再是单纯的调用 User 的全参构造器实例化 User 对象, 而是先判断sex是否有无默认值 。
这就是 Lombok 为我们所做的事情。
所以,我觉得使用 Lombok 的 @Builder
注解的时候,还是要思考一下。当你不需要成员变量不可变的时候,你完全没必要使用构建器模式,因为这会消耗 Java 虚拟机的内存。还有使用 Lombok 的 @Builder
注解时,属性默认值失效问题。
全文完
以下文章您可能也会感兴趣:
了解一下第三方登录
分布式 ID 生成策略
可线性化检查:与 NP 完全问题做斗争
Java 类型系统从入门到放弃
使用 RabbitMQ 实现 RPC
原来你是这样的 Stream —— 浅析 Java Stream 实现原理
分布式锁实践之一:基于 Redis 的实现
ConcurrentHashMap 的 size 方法原理分析
从 ThreadLocal 的实现看散列算法
单元测试 -- 工程师 Style 的测试方法
理解 RabbitMQ Exchange
JVM 揭秘: 一个 class 文件的前世今生
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。
长按左侧二维码关注我们,这里有一群热血青年期待着与您相会。