Guillaume Le Floch
Posted on November 20, 2023
Have you ever seen some declaration of class with a generic type referencing itself ?
public class BaseProject<T extends BaseProject> { ... }
public class FileProject extends BaseProject<FileProject> { ... }
It took me some time to understand the purpose of this. Now that this is clear for me, I decided to write something in order to explain it using a simple example: Builders
.
Introduction
Nowadays, library author provides usually builder
to ease the usage of their objects.
This allows you to easily create complex objects by chaining methods.
A cache can easily be created like:
Cache<String> cache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(100)
.build();
Crafting a builder can be summed up by creating a class (usually nested) that will expose a method for each property with can set in the object. This builder also exposes a build()
method that will call the parent class constructor using data stored in the builder.
How to design a simple builder
Let's say we have the following class:
public class Project {
private final String name;
private final File path;
private final Type type;
...
}
Creating a new instance of this class can easily be done by calling a constructor with the required arguments:
var myProject = new Project(name, path, type);
However, it can become complicated when some parameters are optional, we don't want to have all possible constructors declared in our class.
This is where a builder become really handy.
Let's implement it in our Project
class:
public static class ProjectBuilder {
private String name;
private File path;
private Type type;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder path(File path) {
this.path = path;
return this;
}
public Builder type(Type type) {
this.type = type;
return this;
}
public Project build() {
return new Project(name, path, type);
}
}
This builder can be used in the following way:
var myProject = new ProjectBuilder()
.name("my-project")
.type(Type.File)
.build();
Thus, creating a complex object can be as easy as chaining methods based on property we want to set.
What about inheritance
In some case, the class we try to build may extend some classes, and share some property with a super class.
For example, our project could be more specific, instead of having a type
property we could have a specific class for each type:
public class BaseProject {
protected final String name;
...
}
public class FileProject extends BaseProject {
private final Path path;
...
}
public class UrlProject extends BaseProject {
private final URI link;
...
}
Regarding builder
, we could decline a specific builder for each subtype, such as:
public class FileProjectBuilder {
private String name;
private File path;
public FilePojectBuilder name(String name) {
this.name = name;
return this;
}
public FilePojectBuilder path(File path) {
this.path = path;
return this;
}
public FileProject build() { ... }
}
As we can see with this example, all properties of the BaseProject
must be declared in the builder alongside specific properties.
How could we fix that ?
Well, we could use inheritance, such as:
public abstract class BaseProjectBuilder {
protected String name;
public BaseProjectBuilder name(String name) {
this.name = name;
return this;
}
public abstract Project build();
...
}
public class FileProjectBuilder extends BaseProjectBuilder {
private File path;
public FilePojectBuilder path(File path) {
this.path = path;
return this;
}
@Override
public FileProject build() {
...
}
}
public class UrlProject extends BaseProject {
private URI link;
public UrlPojectBuilder link(URI link) {
this.link = link;
return this;
}
@Override
public UrlProject build() {
...
}
}
With this code, the BaseProjectBuilder
takes care of setting BaseProject
properties and specific implementations take care of setting specific properties.
var myFileProject = new FileProjectBuilder()
.name("my-project")
.path(new Files("/src"))
.build();
Unfortunately, this does not work ... The type returned by the name()
method is a BaseProjectBuilder
while the path
method is part of the FileProjectBuilder
subclass. We just lost the subtype when calling the name()
method.
Can we fix that ? Yes, by using self referencing generics.
First we need to update the BaseProjectBuilder
class definition in the following way:
public abstract class BaseProjectBuilder<T extends BaseProjectBuilder> { ... }
Then all subclass will be updated to specify a type in there definition:
public class FileProjectBuilder extends BaseProjectBuilder<FileProjectBuilder> {
...
}
public class UrlProjectBuilder extends BaseProjectBuilder<UrlProjectBuilder> {
...
}
Thanks to this generic type, we can now implement a self()
method in the BaseProjectBuilder
class and update the problematic name()
method as well:
public abstract class BaseProjectBuilder<T extends BaseProjectBuilder> {
...
public T name(String name) {
this.name = name;
return self();
}
public T self() {
return (T) this;
}
}
Now, the name
method will return the correct type and the following code can be compile:
var myFileProject = new FileProjectBuilder()
.name("my-project")
.path(new Files("/src"))
.build();
What about Lombok ?
Lombok allows you to easily generate Builder
using the @Builder
annotation on the class level.
In that specific case we can define a constructor in the subclass and annotate the constructor directly.
Conclusion
Generics are really powerful and can help a lot when it come to handling types. As well as code organisation, we can also find patterns using generics.
Posted on November 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.