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 ?

public class BaseProject<T extends BaseProject> { ... }

public class FileProject extends BaseProject<FileProject> { ... }
Enter fullscreen mode Exit 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 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();
Enter fullscreen mode Exit 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 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;
    ...
}
Enter fullscreen mode Exit fullscreen mode

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

This builder can be used in the following way:

var myProject = new ProjectBuilder()
           .name("my-project")
           .type(Type.File)
           .build();
Enter fullscreen mode Exit 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 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;
    ... 
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (0)