The author | zeng-guang li

Almond backend engineer. Only bald, can be strong!

I remember reading about the Builder in Effective Java earlier, but didn’t use it very often in actual development. Later, when I used Lombok in my project, I discovered that it had an annotation, @Builder, to generate a Builder for Java beans. So, back to review the next relevant knowledge, sorted out as follows.

0. What is Builder mode?

Builder mode is also called Builder mode or generator mode. It’s a design pattern.

Wikipedia defines it as:

The process of building complex objects is abstracted out (abstract class) so that different implementations of this abstract process can construct objects with different representations (attributes).

A typical UML class diagram for the Builder pattern looks like this:

Role Introduction:

  • Product: Abstract class of a Product.

  • Builder: Abstract class that regulates the building of a product, typically by subclasses that implement concrete component processes.

  • ConcreteBuilder: ConcreteBuilder.

  • Director: Unified assembly process (omitted).

Note the difference from the abstract factory pattern: The abstract factory pattern is similar to a generator in that it can also create complex objects. The main difference is that the generator pattern focuses on constructing a complex object step by step. The abstract factory pattern focuses on multiple families of product objects (simple or complex). The generator returns the product in the last step, whereas for the abstract factory, the product returns immediately.

1. Why use the builder pattern?

If a class has a large number of member variables, we need to provide a full-parameter constructor or a large number of set methods. This makes instance creation and assignment cumbersome and unintuitive. With the constructor, we can make the assignment of a variable a chained call, and the method name of the call corresponds to the name of the member variable. Make object creation and assignment simple and intuitive.

Usage scenarios of Builder mode:

  • The same method, different execution sequence, produce different event results.

  • Multiple parts, or parts, can be assembled into an object but produce different results.

  • The Builder pattern is appropriate when the product class is very complex, or when the order of calls in the product class results in different performance.

  • When initializing an object is particularly complex, such as when there are many parameters, many of which have default values.

2. Sample Usage of Lombok Builder

With Lombok we can quickly create Builder patterns. First, create a Java Bean named User, which is very simple, with only two properties, sex, and name. Where @Builder can automatically generate a Builder for the User object, @toString can automatically generate a toStrng() method for the User object.

@Builder@ToStringpublic class User { private Integer sex; private String name; }Copy the code

Let’s test the builder functionality Lombok provides us automatically:

@test public void builderTest() {User User = user.builder ().name(" kernel ").sex(1).build(); System.out.println(user.toString()); }Copy the code

You’ll see the console print:

User(sex=1, name= almond)Copy the code

As you can see, the print is what we want it to look like, but what does Lombok do for us?

Using Lombok requires careful understanding of what Lombok’s annotations do, otherwise it’s easy to make mistakes.

Decompile user.class generated by Lombok

* Note: Please distinguish between two sets of nouns: “Builder method” and “build method “, “constructor” and “builder “.

//// 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 + ")";        }    }}Copy the code

We through decompilation User. Class, to obtain the above source code (preferably with idea built-in decompiler, JD-GUI decompiler source code is not complete).

We find a static inner class for UserBuilder in the source code, and we actually return an instance of this static inner class when we call the Builder method.

This UserBuilder class has the same member variables as User and has a method named sex, name. These methods, named after the member variable of User, assign to the member variable of UserBuilder and return this.

These methods return this, which is essentially the UserBuilder object from which they were called, or “return the object itself.” By returning the object itself, a chain call to the method is formed.

Look at the Build method, which is a method of the UserBuilder class. It creates a new User object and passes its own member variable value to the User member variable.

So why is this pattern called “builder”, because to create an instance of the User class, you first create the inner class UserBuilder. The UserBuilder, which is the builder, is a transition to creating the User object. Therefore, with this mode, there will be the creation of intermediate instances, which will increase the memory consumption of the virtual machine.

4. Do I have to use constructor mode for chain method assignment?

It is not necessary to use the Builder pattern. Lombok’s @Builder is better suited for final modified member variables, as you can see from the above analysis of Lombok’s Builder principles.

Because final modifies member variables, the values need to be determined at instance creation time. When a class has a large number of member variables, we do not want the user to call the full parameter constructor directly.

So if we have a lot of properties, but we don’t need it to be an immutable object with member variables, do we still need the builder pattern? Personally, no, we can refer to the builder and change the code assignment to chained:

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;    }}Copy the code

What we do is simple: privatize the constructor and instantiate the User object in the build() method.

5. Use the @Builder annotation to pay attention to the default value of the property

Let’s change the User class first:

@Builder@ToStringpublic class User { @NonNull private Integer sex = 1; private String name; }Copy the code

Add an @nonull annotation that requires that the sex attribute not be null. We set the default value of sex to 1, which should be fine.

@test public void builderTest() {User User = user.builder.name ().build(); @test public void builderTest() {User User = user.builder.name (). System.out.println(user.toString()); }Copy the code

We removed the sex property because it was already set to the default value, but the following error was reported when running:

Java. Lang. NullPointerException: sex is marked @ NonNull but is nullCopy the code

The default value for sex is null.

To find out, decompile the user.class file and see what Lombok does. Here is the simplified code:

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); } / /... (code omitted)}}Copy the code

As you can see from the decompiled code, the sex property of User already has a default value of 1. But the inner class UserBuilder does not have a default value, so we call user.Builder () to instantiate the Inner class UserBuilder.

When the userBuilder.build () method is finally called, the UserBuilder attribute is passed to the User class, causing the default value of the User class to be overwritten by the UserBuilder class.

A NullPointerException(“sex is marked @nonNULL but is null”) is raised if sex is marked @nonNULL but is null.

How to solve it?

There’s more than one way to do this, one of which is to use @Builder.default annotations on properties that require Default values

    @NonNull    @Builder.Default    private Integer sex = 1;Copy the code

A second test shows that the output is as expected.

So what does the @Builder.Default annotation do? As usual, decompile user.class and look at the simplified source code:

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); } / /... }}Copy the code

The User class has a static method: $default$sex(), which returns the default value of sex.

The inner class UserBuilder contains a Boolean sex$set variable.

When the constructor is called to set the sex property, it determines whether it is null (because of the @nonull annotation), and if it is not null, it sets the sex property value of the inner UserBuilder class and sets sex$set to true. Sex $set specifies whether sex is the default value.

Finally, the userBuilder.build () method does not simply call the full parameter constructor of User to instantiate the User object, but first checks whether sex has a default value.

That’s what Lombok does for us.

6. Summary

So I think it’s worth thinking about when using Lombok’s @Builder annotation. When you don’t need immutable member variables, you don’t need to use the builder pattern at all, because it consumes Java VIRTUAL machine memory. There is also the issue of invalidation of attribute defaults when using Lombok’s @Builder annotation.

The full text after


You may also be interested in the following articles:

  • Learn about third-party logins

  • Distributed ID generation strategy

  • Linearization check: Struggle with NP-complete problems

  • The Java type system goes from getting started to giving up

  • Use RabbitMQ to implement RPC

  • You are a Stream

  • Distributed lock practice one: implementation based on Redis

  • Analysis of the principle of ConcurrentHashMap size method

  • Hash algorithms from the implementation of ThreadLocal

  • Unit testing – Engineer-style testing

  • Understand the RabbitMQ Exchange

  • JVM Debunked: The history of a class file

We are looking for a Java engineer. Please send your resume to [email protected].

Almond technology station

Long press the left QR code to pay attention to us, here is a group of passionate young people looking forward to meeting with you.