The second Edition of Effective Java, Third Edition was published in 2009, nearly eight years ago, but with Java 6,7,8, Even with the release of 9, the Java language has undergone profound changes. Translated into Chinese here for the first time. For everyone to learn and share. Book source code address: github.com/jbloch/effe… Note that some of the code in the book contains methods based on the Java 9 API, so it is best to download JDK versions later than JDK 9. But Java 9 is only an interim release, so installing JDK 10 is recommended.


Effective Java, Third Edition

Item 2: Use Builder mode when there are too many constructor parameters

Both static factories and constructors have one limitation: they do not scale well to scenarios with many optional parameters. Consider an example representing nutrition facts labels on packaged foods. The labels have several required attributes — recommended servings per serving, serving size and calories per serving, and more than 20 optional attributes — total fat, saturated fat, trans fat, cholesterol, sodium, and more. Most products have non-zero values, with only a few optional attributes.

What constructor or static factory should you write for such a class? Traditionally, programmers have used the telescoping constructor pattern, in which a constructor is provided with only one required argument, another with one optional argument, a third with two optional arguments, and so on, eventually including all optional arguments in the constructor. This is what it looks like in practice. For simplicity, only four optional attributes are shown:

// Telescoping constructor pattern - does not scale well! public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // (per serving) optional private final int fat; // (g/serving) optional private final int sodium; // (mg/serving) optional private final int carbohydrate; // (g/serving) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) { this.servingSize = servingSize; this.servings = servings; this.calories = calories; this.fat = fat; this.sodium = sodium; this.carbohydrate = carbohydrate; }}Copy the code

When you want to create an instance, you can use the shortest argument list constructor that contains all the parameters to be set:

NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
Copy the code

Normally, this constructor call requires a lot of parameters that you don’t want to set, but you have to pass a value for them. In this case, we pass a value of 0 for the FAT attribute. “Only” six parameters may not seem so bad, but as the number of parameters increases, it can quickly spiral out of control.

In short, the scalable constructor pattern works, but when you have a lot of parameters, it’s hard to write client-side code, and it’s hard to read. The reader has no idea what these values mean, and must carefully calculate the parameters to find out. A long list of parameters of the same type can lead to subtle bugs. If the client accidentally reverses two of these parameters, the compiler does not complain, but the program behaves incorrectly at run time (item 51).

When you encounter many optional arguments in a constructor, an alternative is the JavaBeans pattern, in which a no-argument constructor is called to create an object and setter methods are called to set each required and optional argument:

// JavaBeans Pattern - allows inconsistency, mandates mutability public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // Required; no default value private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; }}Copy the code

This pattern does not have the disadvantages of the scale constructor pattern. A bit verbose, but it’s easy to create an instance and easy to read the generated code:

NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
Copy the code

Unfortunately, the JavaBeans pattern itself has serious flaws. Because the constructor is split in multiple calls, javabeans can be in an inconsistent state during construction. This class does not have the option to perform consistency by checking the validity of the construct parameter argument. Trying to use an object in an inconsistent state can lead to errors that are very different from the bug-containing code, making it difficult to debug. A related disadvantage is that the JavaBeans pattern eliminates the possibility of making classes immutable (item 17) and requires additional work on the programmer’s part to ensure thread-safety.

Manually “freezing” the object when its construction is complete and not allowing it to be used before thawing can reduce these disadvantages, but this variant is difficult to use and rarely used in practice. Also, it causes an error at run time because the compiler cannot ensure that the programmer calls the freeze method before using the object.

Fortunately, there is a third option that combines the security of the scalable constructor pattern with the readability of the Javabean pattern. It is a form of Builder mode [Gamma95]. Instead of calling the desired object directly, the client calls the constructor (or static factory), takes all the required parameters, and gets a Builder object. The client then calls setter like methods of the Builder object to set each optional parameter. Finally, the client calls a build method with no arguments to generate the object, which is usually immutable. A Builder is usually a static member class of the class it builds (item 24). Here’s an example of it in practice:

// Builder Pattern public class NutritionFacts { private final int servingSize; private final int servings; private final int calories; private final int fat; private final int sodium; private final int carbohydrate; public static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } public Builder calories(int val) { calories = val; return this; } public Builder fat(int val) { fat = val; return this; } public Builder sodium(int val) { sodium = val; return this; } public Builder carbohydrate(int val) { carbohydrate = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; }}Copy the code

The NutritionFacts class is immutable and all parameter defaults are in one place. Setter methods of the Builder return the Builder itself so that the calls can be chained to produce a smooth API. Here is an example of client code:

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
    .calories(100).sodium(35).carbohydrate(27).build();
Copy the code

The client code is easy to write and, more importantly, easy to read. Builder mode emulates naming in Python and Scala optional parameter.

Validity checking has been omitted for brevity. To detect invalid parameters as soon as possible, check the construction method of the Builder and the validity of parameters in the method. Check the invariance of multiple arguments in the constructor of the build method call. To ensure that these immutability are not attacked, the object properties are checked after the parameters are copied from the Builder (item 50). If the check fails, an IllegalArgumentException exception (item 72) is thrown, with a detailed message indicating which arguments are invalid (Item 75).

The Builder pattern is perfect for class hierarchies. Use parallel layers of Builders, each nested in a corresponding class. Abstract classes have abstract Builders; Concrete classes have concrete Builders. For example, consider an abstract class that represents the root hierarchy of various pizzas:

// Builder pattern for class hierarchies import java.util.EnumSet; import java.util.Objects; import java.util.Set; public abstract class Pizza { public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE} final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); // Subclasses must override this method to return "this" protected abstract T self(); } Pizza(Builder<? > builder) { toppings = builder.toppings.clone(); // See Item 50 } }Copy the code

Note that pizza. Builder is a generic type with recursive type parameters (item 30). This, along with the abstract self method, allows method chains to work properly in subclasses without casting. This workaround for Java’s lack of a self-type is called the idiomatic use of simulated self-type.

There are two specific Pizza subcategories, one of which represents the standard New York-style Pizza and the other is a semicircular grilled cheese pie. The former has a desired size parameter, while the latter allows you to specify whether the sauce should be inside or outside:

import java.util.Objects; public class NyPizza extends Pizza { public enum Size { SMALL, MEDIUM, LARGE } private final Size size; public static class Builder extends Pizza.Builder<Builder> { private final Size size; public Builder(Size size) { this.size = Objects.requireNonNull(size); } @Override public NyPizza build() { return new NyPizza(this); } @Override protected Builder self() { return this; } } private NyPizza(Builder builder) { super(builder); size = builder.size; } } public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> { private boolean sauceInside = false; // Default public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; }}Copy the code

Note that the build method in each subclass Builder is declared to return the correct subclass: the build method in NyPizza.Builder returns NyPizza, while the build method in calzone. builder returns Calzone. This technique, in which a subclass of methods is declared to return a subtype of the return type declared in the superclass, is called covariant return typing. It allows clients to use these Builders without casting.

The client code for these “layered Builders” is essentially the same as the code for the simple NutritionFacts Builder. For brevity, the sample client code shown below assumes static imports of enumeration constants:

NyPizza pizza = new NyPizza.Builder(SMALL)
        .addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
        .addTopping(HAM).sauceInside().build();
Copy the code

A slight advantage of The Builder over constructors is that the Builder can have multiple mutable parameters, because each parameter is specified in its own method. Alternatively, the Builder can aggregate parameters passed to multiple calls into a single property, as demonstrated earlier in the addTopping method.

The Builder mode is very flexible. A single Builder can be reused to build multiple objects. The parameters of the Builder can be adjusted between calls to the build method to change the object created. The Builder can automatically fill in some properties when an object is created, such as the serial number that is added each time the object is created.

The Builder model also has drawbacks. To create an object, you must first create its Builder. While the cost of creating this Builder is unlikely to be noticed in practice, problems can arise in performance critical situations. Also, the Builder pattern is more verbose than the flex constructor pattern, so it’s only worth using if you have enough parameters, such as four or more. But keep in mind that if you want to add more parameters in the future. However, if you start with a constructor or static factory and switch to builder, the outdated constructor or static factory will be in an awkward position when the class evolves to a point where the number of parameters gets out of control. Therefore, therefore, it is best to create a Builder from the beginning.

In summary, The Builder pattern is a good choice when designing a class’s constructor or static factory with more than a few parameters, especially if many of the parameters are optional or of the same type. Client code is easier to read and write than using telescoping Constructors, and Builders are more secure than JavaBeans.