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

栏目: Java · 发布时间: 6年前

内容简介: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 注解时,属性默认值失效问题。

全文完

以下文章您可能也会感兴趣:

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

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

杏仁技术站

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Google's PageRank and Beyond

Google's PageRank and Beyond

Amy N. Langville、Carl D. Meyer / Princeton University Press / 2006-7-23 / USD 57.50

Why doesn't your home page appear on the first page of search results, even when you query your own name? How do other web pages always appear at the top? What creates these powerful rankings? And how......一起来看看 《Google's PageRank and Beyond》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具