转载

Lombok Builder 构建器做了哪些事情?

作者 | 李增光

Lombok Builder 构建器做了哪些事情?

杏仁后端工程师。 只有变秃,才能变强!

记得之前,我在《effective java》上看过 Builder 构建器相关的内容,但实际开发中不经常用。后来,在项目中使用了 lombok ,发现它有一个注解   @Builder ,就是为 java bean 生成一个构建器。于是,回头重新复习了下相关知识,整理如下。

0.何为 Builder 模式 ?

Builder 模式 又被称作 建造者模式   或者   生成器模式 .是一种设计模式。

维基百科上的定义为:

将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

一个典型的 Builder 模式的 UML 类图如下:

Lombok Builder 构建器做了哪些事情?

角色介绍:

  • Product 产品类 : 产品的抽象类。

  • Builder : 抽象类, 规范产品的组建,一般是由子类实现具体的组件过程。

  • ConcreteBuilder : 具体的构建器。

  • Director : 统一组装过程(可省略)。

注意与抽象工厂模式的区别:   抽象工厂模式与生成器相似,因为它也可以创建复杂对象.主要的区别是生成器模式着重于一步步构造一个复杂对象.而抽象工厂模式着重于多个系列的产品对象(简单的或是复杂的).生成器在最后的一步返回产品,而对于抽象工厂来说,产品是立即返回的。

1. 为什么使用构建器模式 ?

若一个类具有大量的成员变量,我们就需要提供一个全参的构造器或大量的 set 方法.这让实例的创建和赋值,变得很麻烦,且不直观.我们通过构建器,可以让变量的赋值变成链式调用,而且调用的方法名对应着成员变量的名称.让对象的创建和赋值都变得很简洁、直观。

Builder 模式的使用场景:

  • 相同的方法,不同的执行顺序,产生不同的事件结果时。

  • 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时。

  • 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适。

  • 当初始化一个对象特别复杂,如参数多,且很多参数都具有默认值时。

2. lombok 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 的注解做了什么,否则很容易出错。

3. 反编译 Lombok 生成的 User.class

*注意:下面请区分两组名词:"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相同的成员变量,且拥有名为 sexname 的方法.这些以 User 的成员变量命名的方法,都是给 UserBuilder 的成员变量赋值,并返回 this 。

这些方法返回 this,其实就是返回调用这些方法的 UserBuilder 对象,也可称为“返回对象本身”。通过返回对象本身,形成了方法的链式调用。

再看 build 方法,它是 UserBuilder 类的方法.它创建了一个新的 User 对象,并将自身的成员变量值,传给了User 的成员变量。

所以为什么这种模式叫''构建器'',因为要创建 User 类的实例,首先要创建内部类 UserBuilder 。而这个UserBuilder 也就是构建器,是创建 User 对象的一个过渡者. 所以利用这种模式,会有中间实例的创建,会加大虚拟机内存的消耗。

4.链式方法赋值,一定要用构建器模式吗?

不一定要用到构建器模式,通过上面分析 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 对象。

5. 使用 @Builder 注解需要注意的 属性默认值问题

我们先来改造一下 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 为我们所做的事情。

6. 总结

所以,我觉得使用 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 。

Lombok Builder 构建器做了哪些事情?

杏仁技术站

长按左侧二维码关注我们,这里有一群热血青年期待着与您相会。

原文  http://mp.weixin.qq.com/s?__biz=MzUxOTE5MTY4MQ==&mid=2247484184&idx=1&sn=79eb3109d7527e4ba294a0fa6c15cf03
正文到此结束
Loading...