Have you ever seen some declaration of class with a generic type referencing itself ?
publicclassBaseProject<TextendsBaseProject>{...}publicclassFileProjectextendsBaseProject<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 usuallybuilder
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 abuild()
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:
publicclassProject{privatefinalStringname;privatefinalFilepath;privatefinalTypetype;...}
Creating a new instance of this class can easily be done by calling a constructor with the required arguments:
varmyProject=newProject(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 ourProject
class:
publicstaticclassProjectBuilder{privateStringname;privateFilepath;privateTypetype;publicBuildername(Stringname){this.name=name;returnthis;}publicBuilderpath(Filepath){this.path=path;returnthis;}publicBuildertype(Typetype){this.type=type;returnthis;}publicProjectbuild(){returnnewProject(name,path,type);}}
This builder can be used in the following way:
varmyProject=newProjectBuilder().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 atype
property we could have a specific class for each type:
publicclassBaseProject{protectedfinalStringname;...}publicclassFileProjectextendsBaseProject{privatefinalPathpath;...}publicclassUrlProjectextendsBaseProject{privatefinalURIlink;...}
Regardingbuilder
, we could decline a specific builder for each subtype, such as:
publicclassFileProjectBuilder{privateStringname;privateFilepath;publicFilePojectBuildername(Stringname){this.name=name;returnthis;}publicFilePojectBuilderpath(Filepath){this.path=path;returnthis;}publicFileProjectbuild(){...}}
As we can see with this example,all properties of theBaseProject
must be declared in the builder alongside specific properties.
How could we fix that ?
Well, we could use inheritance, such as:
publicabstractclassBaseProjectBuilder{protectedStringname;publicBaseProjectBuildername(Stringname){this.name=name;returnthis;}publicabstractProjectbuild();...}publicclassFileProjectBuilderextendsBaseProjectBuilder{privateFilepath;publicFilePojectBuilderpath(Filepath){this.path=path;returnthis;}@OverridepublicFileProjectbuild(){...}}publicclassUrlProjectextendsBaseProject{privateURIlink;publicUrlPojectBuilderlink(URIlink){this.link=link;returnthis;}@OverridepublicUrlProjectbuild(){...}}
With this code, theBaseProjectBuilder
takes care of settingBaseProject
properties and specific implementations take care of setting specific properties.
varmyFileProject=newFileProjectBuilder().name("my-project").path(newFiles("/src")).build();
Unfortunately, this does not work ... The type returned by thename()
method is aBaseProjectBuilder
while thepath
method is part of theFileProjectBuilder
subclass. We just lost the subtype when calling thename()
method.
Can we fix that ?Yes, by using self referencing generics.
First we need to update theBaseProjectBuilder
class definition in the following way:
publicabstractclassBaseProjectBuilder<TextendsBaseProjectBuilder>{... }
Then all subclass will be updated to specify a type in there definition:
publicclassFileProjectBuilderextendsBaseProjectBuilder<FileProjectBuilder>{...}publicclassUrlProjectBuilderextendsBaseProjectBuilder<UrlProjectBuilder>{...}
Thanks to this generic type, we can now implement aself()
method in theBaseProjectBuilder
class and update the problematicname()
method as well:
publicabstractclassBaseProjectBuilder<TextendsBaseProjectBuilder>{...publicTname(Stringname){this.name=name;returnself();}publicTself(){return(T)this;}}
Now, thename
method will return the correct type and the following code can be compile:
varmyFileProject=newFileProjectBuilder().name("my-project").path(newFiles("/src")).build();
What about Lombok ?
Lombok allows you to easily generateBuilder
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.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse