Skip to content

Latest commit

 

History

History
565 lines (444 loc) · 18.2 KB

autobuilder.md

File metadata and controls

565 lines (444 loc) · 18.2 KB

AutoBuilder

AutoBuilder makes it easy to create a generalized builder, with setter methods that accumulate values, and a build method that calls a constructor or static method with those values as parameters. Callers don't need to know the order of those parameters. Parameters can also have default values. There can be validation before the constructor or method call.

If you are familiar with AutoValue builders then AutoBuilder should also be familiar. Where an @AutoValue.Builder has setter methods corresponding to the getter methods in the @AutoValue class, an @AutoBuilder has setter methods corresponding to the parameters of a constructor or static method. Apart from that, the two are very similar.

Example: calling a constructor

Here is a simple example:

@AutoBuilder(ofClass = Person.class)
abstract class PersonBuilder {
  static PersonBuilder personBuilder() {
    return new AutoBuilder_PersonBuilder();
  }

  abstract PersonBuilder setName(String name);
  abstract PersonBuilder setId(int id);
  abstract Person build();
}

It might be used like this:

Person p = PersonBuilder.personBuilder().setName("Priz").setId(6).build();

That would have the same effect as this:

Person p = new Person("Priz", 6);

But it doesn't require you to know what order the constructor parameters are in.

Here, setName and setId are setter methods. Calling builder.setName("Priz") records the value "Priz" for the parameter name, and likewise with setId.

There is also a build() method. Calling that method invokes the Person constructor with the parameters that were previously set.

Example: calling a Kotlin constructor

Kotlin has named arguments and default arguments for constructors and functions, which means there is not much need for anything like AutoBuilder there. But if you are constructing an instance of a Kotlin data class from Java code, AutoBuilder can help.

Given this trivial Kotlin data class:

class KotlinData(val level: Int, val name: String?, val id: Long = -1L)

You might make a builder for it like this:

@AutoBuilder(ofClass = KotlinData.class)
public abstract class KotlinDataBuilder {
  public static KotlinDataBuilder kotlinDataBuilder() {
    return new AutoBuilder_KotlinDataBuilder();
  }

  public abstract KotlinDataBuilder setLevel(int x);
  public abstract KotlinDataBuilder setName(@Nullable String x);
  public abstract KotlinDataBuilder setId(long x);
  public abstract KotlinData build();
}

The Kotlin type String? corresponds to @Nullable String in the AutoBuilder class, where @Nullable is any annotation with that name, such as org.jetbrains.annotations.Nullable.

The id parameter has a default value of -1L, which means that if setId is not called then the id field of the built KotlinData will be -1L.

If you are using kapt then you can also define the builder in the data class itself:

class KotlinData(val level: Int, val name: String?, val id: Long = -1L) {
  @AutoBuilder // we don't need ofClass: by default it is the containing class
  interface Builder {
    fun setLevel(x: Int): Builder
    fun setName(x: String?): Builder
    fun setId(x: Long): Builder
    fun build(): KotlinData
  }

  fun toBuilder(): Builder = AutoBuilder_KotlinData_Builder(this)

  companion object {
    @JvmStatic fun builder(): Builder = AutoBuilder_KotlinData_Builder()
  }
}

This example uses an interface rather than an abstract class for the builder, but both are possible. Java code would then construct instances like this:

KotlinData k = KotlinData.builder().setLevel(23).build();

The example also implements a toBuilder() method to get a builder that starts out with values from the given instance. See below for more details on that.

The generated subclass

Like @AutoValue.Builder, compiling an @AutoBuilder class will generate a concrete subclass. In the example above, this will be class AutoBuilder_PersonBuilder extends PersonBuilder. It is common to have a static builder() method, as in the example, which calls new AutoBuilder_...(). That will typically be the only reference to the generated class.

If the @AutoBuilder type is nested then the name of the generated class reflects that nesting. For example:

class Outer {
  static class Inner {
    @AutoBuilder
    abstract static class Builder {...}
  }
  static Inner.Builder builder() {
    return new AutoBuilder_Outer_Inner_Builder();
  }
}

@AutoBuilder annotation parameters

@AutoBuilder has two annotation parameters, ofClass and callMethod.

If ofClass is specified, then build() will call a constructor or static method of that class. Otherwise it will call a constructor or static method of the class containing the @AutoBuilder class.

If callMethod is specified, then build() will call a static method with that name. Otherwise build() will call a constructor.

The following examples illustrate the various possibilities. These examples use an interface for the @AutoBuilder type. You can also use an abstract class; if it is nested then it must be static.

Both callMethod and ofClass

@AutoBuilder(callMethod = "of", ofClass = LocalTime.class)
interface LocalTimeBuilder {
  ...
  LocalTime build(); // calls: LocalTime.of(...)
}

Only ofClass

@AutoBuilder(ofClass = Thread.class)
interface ThreadBuilder {
  ...
  Thread build(); // calls: new Thread(...)
}

Only callMethod

class Foo {
  static String concat(String first, String middle, String last) {...}

  @AutoBuilder(callMethod = "concat")
  interface ConcatBuilder {
    ...
    String build(); // calls: Foo.concat(first, middle, last)
  }
}

Notice in this example that the static method returns String. The implicit ofClass is Foo, but the static method can return any type.

Neither callMethod nor ofClass

class Person {
  Person(String name, int id) {...}

  @AutoBuilder
  interface Builder {
    ...
    Person build(); // calls: new Person(name, id)
  }
}

The build method

The build method must have a certain return type. If it calls a constructor then its return type must be the type of the constructed class. If it calls a static method then its return type must be the return type of the static method.

The build method is often called build() but it does not have to be. The only requirement is that there must be exactly one no-arg abstract method that has the return type just described and that does not correspond to a parameter name.

The following example uses the name call() since that more accurately reflects what it does:

public class LogUtil {
  public static void log(Level severity, String message, Object... params) {...}

  @AutoBuilder(callMethod = "log")
  public interface Caller {
    Caller setSeverity(Level level);
    Caller setMessage(String message);
    Caller setParams(Object... params);
    void call(); // calls: LogUtil.log(severity, message, params)
  }

Making a builder from a built instance

It is not always possible to map back from the result of a constructor or method call to a builder that might have produced it. But in one important case, it is possible. That's when every parameter in the constructor or method corresponds to a "getter method" in the built type. This will always be true when building a Java record or a Kotlin data class (provided its getters are visible to the builder). In this case, the generated builder class will have a second constructor that takes an object of the built type as a parameter and produces a builder that starts out with values from that object. That can then be used to produce a new object that may differ from the first one in just one or two properties. (This is very similar to AutoValue's toBuilder() feature.)

If the constructor or method has a parameter String bar then the built type must have a visible method String bar() or String getBar(). (Java records have the first and Kotlin data classes have the second.) If there is a similar corresponding method for every parameter then the second constructor is generated.

If you are able to change the built type, the most convenient way to use this is to add a toBuilder() instance method that calls new AutoBuilder_Foo(this). We saw this in the Kotlin example earlier. Otherwise, you can have a second static builder method, like this:

@AutoBuilder(ofClass = Person.class)
abstract class PersonBuilder {
  static PersonBuilder personBuilder() {
    return new AutoBuilder_PersonBuilder();
  }
  static PersonBuilder personBuilder(Person person) {
    return new AutoBuilder_PersonBuilder(person);
  }
  ...
}

Overloaded constructors or methods

There might be more than one constructor or static method that matches the callMethod and ofClass. AutoBuilder will ignore any that are not visible to the generated class, meaning private, or package-private and in a different package. Of the others, it will pick the one whose parameter names match the @AutoBuilder setter methods. It is a compilation error if there is not exactly one such method or constructor.

Generics

If the builder calls the constructor of a generic type, then it must have the same type parameters as that type, as in this example:

class NumberPair<T extends Number> {
  NumberPair(T first, T second) {...}

  @AutoBuilder
  interface Builder<T extends Number> {
    Builder<T> setFirst(T x);
    Builder<T> setSecond(T x);
    NumberPair<T> build();
  }
}

If the builder calls a static method with type parameters, then it must have the same type parameters, as in this example:

class Utils {
  static <K extends Number, V> Map<K, V> singletonNumberMap(K key, V value) {...}

  @AutoBuilder(callMethod = "singletonNumberMap")
  interface Builder<K extends Number, V> {
    Builder<K, V> setKey(K x);
    Builder<K, V> setValue(V x);
    Map<K, V> build();
  }
}

Although it's unusual, a Java constructor can have its own type parameters, separately from any that its containing class might have. A builder that calls a constructor like that must have the type parameters of the class followed by the type parameters of the constructor:

class CheckedSet<E> implements Set<E> {
  <T extends E> CheckedSet(Class<T> type) {...}

  @AutoBuilder
  interface Builder<E, T extends E> {
    Builder<E, T> setType(Class<T> type);
    CheckedSet<E> build();
  }
}

Required, optional, and nullable parameters

Parameters that are annotated @Nullable are null by default. Parameters of type Optional, OptionalInt, OptionalLong, and OptionalDouble are empty by default. Kotlin constructor parameters with default values get those values by default. Every other parameter is required, meaning that the build method will throw IllegalStateException if any are omitted.

To establish default values for parameters, set them in the builder() method before returning the builder.

class Foo {
  Foo(String bar, @Nullable String baz, String buh) {...}

  static Builder builder() {
    return new AutoBuilder_Foo_Builder()
        .setBar(DEFAULT_BAR);
  }

  @AutoBuilder
  interface Builder {
    Builder setBar(String x);
    Builder setBaz(String x);
    Builder setBuh(String x);
    Foo build();
  }

  {
     builder().build(); // IllegalStateException, buh is not set
     builder().setBuh("buh").build(); // OK, bar=DEFAULT_BAR and baz=null
     builder().setBaz(null).setBuh("buh").build(); // OK
     builder().setBar(null); // NullPointerException, bar is not @Nullable
  }
}

Trying to set a parameter that is not annotated @Nullable to null will produce a NullPointerException.

@Nullable here is any annotation with that name, such as javax.annotation.Nullable or org.checkerframework.checker.nullness.qual.Nullable.

Getters

The @AutoBuilder class or interface can also have getter methods. A getter method returns the value that has been set for a certain parameter. Its return type can be either the same as the parameter type, or an Optional wrapping that type. Calling the getter before any value has been set will throw an exception in the first case or return an empty Optional in the second.

In this example, the nickname parameter defaults to the same value as the name parameter but can also be set to a different value:

public class Named {
  Named(String name, String nickname) {...}

  @AutoBuilder
  public abstract static class Builder {
    public abstract Builder setName(String x);
    public abstract Builder setNickname(String x);
    abstract String getName();
    abstract Optional<String> getNickname();
    abstract Named autoBuild();

    public Named build() {
      if (!getNickname().isPresent()) {
        setNickname(getName());
      }
      return autoBuild();
    }
  }
}

The example illustrates having a package-private autoBuild() method that AutoBuilder implements. The public build() method calls it after adjusting the nickname if necessary.

The builder in the example is an abstract class rather than an interface. An abstract class allows us to distinguish between public methods for users of the builder to call, and package-private methods that the builder's own logic uses.

Building annotation instances

AutoBuilder can build instances of annotation interfaces. When the annotation has no elements (methods in the annotation), or only one, then AutoAnnotation is simpler to use. But when there are several elements, a builder is helpful. See here for examples of both.

Naming conventions

A setter method for the parameter foo can be called either setFoo or foo. A getter method can be called either getFoo or foo, and for a boolean parameter it can also be called isFoo. The choice for getters and setters is independent. For example your getter might be foo() while your setter is setFoo(T).

By convention, the parameter name of a setter method either echoes the parameter being set:
Builder setName(String name);
or it is just x:
Builder setName(String x);

If class Foo has a nested @AutoBuilder that builds instances of Foo, then conventionally that type is called Builder, and instances of it are obtained by calling a static Foo.builder() method:

Foo foo1 = Foo.builder().setBar(bar).setBaz(baz).build();
Foo.Builder fooBuilder = Foo.builder();

If an @AutoBuilder for Foo is its own top-level class then that class will typically be called FooBuilder and it will have a static fooBuilder() method that returns an instance of FooBuilder. That way callers can statically import FooBuilder.fooBuilder and just write fooBuilder() in their code.

@AutoBuilder(ofClass = Foo.class)
public abstract class FooBuilder {
  public static FooBuilder fooBuilder() {
    return new AutoBuilder_FooBuilder();
  }
  ...
  public abstract Foo build();
}

If an @AutoBuilder is designed to call a static method that is not a factory method, the word "call" is better than "build" in the name of the type (FooCaller), the static method (fooCaller()), and the "build" method (call()).

@AutoBuilder(callMethod = "log", ofClass = MyLogger.class)
public abstract class LogCaller {
  public static LogCaller logCaller() {
    return new AutoBuilder_LogCaller();
  }
  ...
  public abstract void call();
}

// used as:
logCaller().setLevel(Level.INFO).setMessage("oops").call();

Other builder features

There are a number of other builder features that have not been detailed here because they are the same as for @AutoValue.Builder. They include:

When parameter names are unavailable

AutoBuilder depends on knowing the names of parameters. But parameter names are not always available in Java. They are available in these cases, at least:

  • In code that is being compiled at the same time as the @AutoBuilder class or interface.
  • In records (from Java 16 onwards).
  • In the constructors of Kotlin data classes.
  • In code that was compiled with the -parameters option.

A Java compiler bug means that parameter names are not available to AutoBuilder when compiling with JDK versions before 11, in any of these cases except the first. We recommend building with a recent JDK, using the --release option if necessary to produce code that can run on earlier versions.

If parameter names are unavailable, you always have the option of introducing a static method in the same class as the @AutoBuilder type, and having it call the method you want. Since it is compiled at the same time, its parameter names are available.

Here's an example of fixing a problem this way. The code here typically will not compile, since parameter names of JDK methods are not available:

import java.time.LocalTime;

public class TimeUtils {
  // Does not work, since parameter names from LocalTime.of are unavailable.
  @AutoBuilder(callMethod = "of", ofClass = LocalTime.class)
  public interface TimeBuilder {
    TimeBuilder setHour(int x);
    TimeBuilder setMinute(int x);
    TimeBuilder setSecond(int x);
    LocalTime build();
  }
}

It will produce an error message like this:

error: [AutoBuilderNoMatch] Property names do not correspond to the parameter names of any static method named "of":
  public interface TimeBuilder {
  ^
    of(int arg0, int arg1)
    of(int arg0, int arg1, int arg2)
    of(int arg0, int arg1, int arg2, int arg3)

The names arg0, arg1, etc are concocted by the compiler because it doesn't have the real names.

Introducing a static method fixes the problem:

import java.time.LocalTime;

public class TimeUtils {
  static LocalTime localTimeOf(int hour, int minute, int second) {
    return LocalTime.of(hour, minute, second);
  }

  @AutoBuilder(callMethod = "localTimeOf")
  public interface TimeBuilder {
    TimeBuilder setHour(int x);
    TimeBuilder setMinute(int x);
    TimeBuilder setSecond(int x);
    LocalTime build();
  }
}