无法为属性或索引器赋值_Lombok Builder 构建器做了哪些事情?

f38a0bd962faa3dcc901aebf45cbc2ff.png

作者 | 李增光

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

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

0.何为 Builder 模式 ?

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

维基百科上的定义为:

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

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

c37485498e84fe0063e90dbc0acf67e4.png

角色介绍:

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

欢迎搜索关注微信公众号:杏仁技术站(微信号 xingren-tech)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值