Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Guillaume Le Floch
Guillaume Le Floch

Posted on

     

Self-referencing generics, wait, what ?

Have you ever seen some declaration of class with a generic type referencing itself ?

publicclassBaseProject<TextendsBaseProject>{...}publicclassFileProjectextendsBaseProject<FileProject>{...}
Enter fullscreen modeExit fullscreen mode

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();
Enter fullscreen modeExit fullscreen mode

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;...}
Enter fullscreen modeExit fullscreen mode

Creating a new instance of this class can easily be done by calling a constructor with the required arguments:

varmyProject=newProject(name,path,type);
Enter fullscreen modeExit fullscreen mode

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);}}
Enter fullscreen modeExit fullscreen mode

This builder can be used in the following way:

varmyProject=newProjectBuilder().name("my-project").type(Type.File).build();
Enter fullscreen modeExit fullscreen mode

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;...}
Enter fullscreen modeExit fullscreen mode

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(){...}}
Enter fullscreen modeExit fullscreen mode

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(){...}}
Enter fullscreen modeExit fullscreen mode

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();
Enter fullscreen modeExit fullscreen mode

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>{... }
Enter fullscreen modeExit fullscreen mode

Then all subclass will be updated to specify a type in there definition:

publicclassFileProjectBuilderextendsBaseProjectBuilder<FileProjectBuilder>{...}publicclassUrlProjectBuilderextendsBaseProjectBuilder<UrlProjectBuilder>{...}
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

Now, thename method will return the correct type and the following code can be compile:

varmyFileProject=newFileProjectBuilder().name("my-project").path(newFiles("/src")).build();
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

  • Work
    Software developer at Zenika
  • Joined

More fromGuillaume Le Floch

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp