1. Overview

In this tutorial, we’ll use the FreeBuilder library to generate builder classes in Java.

2. Builder Design Pattern

Builder is one of the most widely used Creation Design Patterns in object-oriented languages. It abstracts the instantiation of a complex domain object and provides a fluent API for creating an instance. It thereby helps to maintain a concise domain layer.

Despite its usefulness, a builder is generally complex to implement, particularly in Java. Even simpler value objects require a lot of boilerplate code.

3. Builder Implementation in Java

Before we proceed with FreeBuilder, let’s implement a boilerplate builder for our Employee class:

public class Employee {

    private final String name;
    private final int age;
    private final String department;

    private Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }
}

And an inner Builder class:

public static class Builder {

    private String name;
    private int age;
    private String department;

    public Builder setName(String name) {
        this.name = name;
        return this;
    }

    public Builder setAge(int age) {
        this.age = age;
        return this;
    }

    public Builder setDepartment(String department) {
        this.department = department;
        return this;
    }

    public Employee build() {
        return new Employee(name, age, department);
    }
}

Accordingly, we can now use the builder for instantiating the Employee object:

Employee.Builder emplBuilder = new Employee.Builder();

Employee employee = emplBuilder
  .setName("baeldung")
  .setAge(12)
  .setDepartment("Builder Pattern")
  .build();

As shown above, a lot of boilerplate code is necessary for implementing a builder class.

In the later sections, we’ll see how FreeBuilder can instantly simplify this implementation.

4. Maven Dependency

To add the FreeBuilder library, we’ll add the FreeBuilder Maven dependency in our pom.xml:

<dependency>
    <groupId>org.inferred</groupId>
    <artifactId>freebuilder</artifactId>
    <version>2.4.1</version>
</dependency>

5. FreeBuilder Annotation

5.1. Generating a Builder

FreeBuilder is an open-source library that helps developers avoid the boilerplate code while implementing builder classes. It makes use of annotation processing in Java to generate a concrete implementation of the builder pattern.

We’ll **annotate our Employee class from the earlier section with *@***FreeBuilder and see how it automatically generates the builder class:

@FreeBuilder
public interface Employee {
 
    String name();
    int age();
    String department();
    
    class Builder extends Employee_Builder {
    }
}

It’s important to point out that Employee is now an interface rather than a POJO class. Furthermore, it contains all the attributes of an Employee object as methods.

Before we continue to use this builder, we must configure our IDEs to avoid any compilation issues. Since FreeBuilder automatically generates the Employee_Builder class during compilation, the IDE usually complains of ClassNotFoundException on line number 8.

To avoid such issues, we need to enable annotation processing in IntelliJ or Eclipse. And while doing so, we’ll use FreeBuilder’s annotation processor org.inferred.freebuilder.processor.Processor. Additionally, the directory used for generating these source files should be marked as Generated Sources Root.

Alternatively, we can also execute mvn install to build the project and generate the required builder classes.

Finally, we have compiled our project and can now use the Employee.Builder class:

Employee.Builder builder = new Employee.Builder();
 
Employee employee = builder.name("baeldung")
  .age(10)
  .department("Builder Pattern")
  .build();

All in all, there are two main differences between this and the builder class we saw earlier. First, we must set the value for all attributes of the Employee class. Otherwise, it throws an IllegalStateException.

We’ll see how FreeBuilder handles optional attributes in a later section.

Second, the method names of Employee.Builder don’t follow the JavaBean naming conventions. We’ll see this in the next section.

5.2. JavaBean Naming Convention

To enforce FreeBuilder to follow the JavaBean naming convention, we must rename our methods in Employee and prefix the methods with get:

@FreeBuilder
public interface Employee {
 
    String getName();
    int getAge();
    String getDepartment();

    class Builder extends Employee_Builder {
    }
}

This will generate getters and setters that follow the JavaBean naming convention:

Employee employee = builder
  .setName("baeldung")
  .setAge(10)
  .setDepartment("Builder Pattern")
  .build();

5.3. Mapper Methods

Coupled with getters and setters, FreeBuilder also adds mapper methods in the builder class. These mapper methods accept a UnaryOperator as input, thereby allowing developers to compute complex field values.

Suppose our Employee class also has a salary field:

@FreeBuilder
public interface Employee {
    Optional<Double> getSalaryInUSD();
}

Now suppose we need to convert the currency of the salary that is provided as input:

long salaryInEuros = INPUT_SALARY_EUROS;
Employee.Builder builder = new Employee.Builder();

Employee employee = builder
  .setName("baeldung")
  .setAge(10)
  .mapSalaryInUSD(sal -> salaryInEuros * EUROS_TO_USD_RATIO)
  .build();

FreeBuilder provides such mapper methods for all fields.

6. Default Values and Constraint Checks

6.1. Setting Default Values

The Employee.Builder implementation we have discussed so far expects the client to pass values for all fields. As a matter of fact, it fails the initialization process with an IllegalStateException in case of missing fields.

In order to avoid such failures, we can either set default values for fields or make them optional.

We can set default values in the Employee.Builder constructor:

@FreeBuilder
public interface Employee {

    // getter methods

    class Builder extends Employee_Builder {

        public Builder() {
            setDepartment("Builder Pattern");
        }
    }
}

So we simply set the default department in the constructor. This value will apply to all Employee objects.

6.2. Constraint Checks

Usually, we have certain constraints on field values. For example, a valid email must contain an “@” or the age of an Employee must be within a range.

Such constraints require us to put validations on input values. And FreeBuilder allows us to add these validations by merely overriding the setter methods:

@FreeBuilder
public interface Employee {

    // getter methods

    class Builder extends Employee_Builder {

        @Override
        public Builder setEmail(String email) {
            if (checkValidEmail(email))
                return super.setEmail(email);
            else
                throw new IllegalArgumentException("Invalid email");

        }

        private boolean checkValidEmail(String email) {
            return email.contains("@");
        }
    }
}

7. Optional Values

7.1. Using Optional Fields

Some objects contain optional fields, the values for which can be empty or null. FreeBuilder allows us to define such fields using the Java Optional type:

@FreeBuilder
public interface Employee {

    String getName();
    int getAge();

    // other getters
    
    Optional<Boolean> getPermanent();

    Optional<String> getDateOfJoining();

    class Builder extends Employee_Builder {
    }
}

Now we may skip providing any value for Optional fields:

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .setPermanent(true)
  .build();

Notably, we simply passed the value for permanent field instead of an Optional. Since we didn’t set the value for dateOfJoining field, it will be Optional.empty() which is the default for Optional fields.

7.2. Using @Nullable Fields

Although using Optional is recommended for handling nulls in Java, FreeBuilder allows us to use @Nullable for backward compatibility:

@FreeBuilder
public interface Employee {

    String getName();
    int getAge();
    
    // other getter methods

    Optional<Boolean> getPermanent();
    Optional<String> getDateOfJoining();

    @Nullable String getCurrentProject();

    class Builder extends Employee_Builder {
    }
}

The use of Optional is ill-advised in some cases which is another reason why @Nullable is preferred for builder classes.

8. Collections and Maps

FreeBuilder has special support for collections and maps:

@FreeBuilder
public interface Employee {

    String getName();
    int getAge();
    
    // other getter methods

    List<Long> getAccessTokens();
    Map<String, Long> getAssetsSerialIdMapping();


    class Builder extends Employee_Builder {
    }
}

FreeBuilder adds convenience methods to add input elements into the Collection in the builder class:

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .addAccessTokens(1221819L)
  .addAccessTokens(1223441L, 134567L)
  .build();

There is also a getAccessTokens() method in the builder class which returns an unmodifiable list. Similarly, for Map:

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .addAccessTokens(1221819L)
  .addAccessTokens(1223441L, 134567L)
  .putAssetsSerialIdMapping("Laptop", 12345L)
  .build();

The getter method for Map also returns an unmodifiable map to the client code.

9. Nested Builders

For real-world applications, we may have to nest a lot of value objects for our domain entities. And since the nested objects can themselves need builder implementations, FreeBuilder allows nested buildable types.

For example, suppose we have a nested complex type Address in the Employee class:

@FreeBuilder
public interface Address {
 
    String getCity();

    class Builder extends Address_Builder {
    }
}

Now, FreeBuilder generates setter methods that take Address.Builder as an input together with Address type:

Address.Builder addressBuilder = new Address.Builder();
addressBuilder.setCity(CITY_NAME);

Employee employee = builder.setName("baeldung")
  .setAddress(addressBuilder)
  .build();

Notably, FreeBuilder also adds a method to customize the existing Address object in the Employee:

Employee employee = builder.setName("baeldung")
  .setAddress(addressBuilder)
  .mutateAddress(a -> a.setPinCode(112200))
  .build();

Along with FreeBuilder types, FreeBuilder also allows nesting of other builders such as protos.

10. Building Partial Object

As we’ve discussed before, FreeBuilder throws an IllegalStateException for any constraint violation — for instance, missing values for mandatory fields.

Although this is desired for production environments, it complicates unit testing that is independent of constraints in general.

To relax such constraints, FreeBuilder allows us to build partial objects:

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .setEmail("[email protected]")
  .buildPartial();

assertNotNull(employee.getEmail());

So, even though we haven’t set all the mandatory fields for an Employee, we could still verify that the email field has a valid value.

11. Custom toString() Method

With value objects, we often need to add a custom toString() implementation. FreeBuilder allows this through abstract classes:

@FreeBuilder
public abstract class Employee {

    abstract String getName();

    abstract int getAge();

    @Override
    public String toString() {
        return getName() + " (" + getAge() + " years old)";
    }

    public static class Builder extends Employee_Builder{
    }
}

We declared Employee as an abstract class rather than an interface and provided a custom toString() implementation.

12. Comparison with Other Builder Libraries

The builder implementation we have discussed in this article is very similar to those of Lombok, Immutables, or any other annotation processor. However, there are a few distinguishing characteristics that we have discussed already:

    • Mapper methods
    • Nested Buildable Types
    • Partial Objects

13. Conclusion

In this article, we used the FreeBuilder library to generate a builder class in Java. We implemented various customizations of a builder class with the help of annotations, thus reducing the boilerplate code required for its implementation.

We also saw how FreeBuilder is different from some of the other libraries and briefly discussed some of those characteristics in this article.

All the code examples are available over on GitHub.


« 上一篇: Java SASL 介绍
» 下一篇: Java 'Hello World' 示例