Polymorphism using generics in java.
This article is for junior developers who do not want to copy-paste their code over and over again for different cases.
Subtyping
Understanding the subtyping in java will help you to build more flexible software architecture. So let’s start.
Consider the following class hierarchy

On the picture you can see that Log is subtype of BaseLog and BaseLog is subtype of Object. It can be written in more elegant way:
Log <: BaseLog <: Object1. Arrays subtyping.
Arrays are covariant in Java. It means that
if the
Ais a subtype of theB, then thearrayofAis subtype of thearrayofB
if A <: B then A[] <: B[]2. Generics subtyping.
Generics are invariant in Java. It means that
if the
Ais subtype of theB, thenGeneric<A>is NOT a subtype of theGeneric<B>.
if A <: B then Generic<A> !<: Generic<B>Let’s demonstrate this 2 rules on our example with Logs. You have 2 methods that will perform adding the array or the list of logs to global storage of logs (Logger).
void addLogs(BaseLog[] logs){/*...*/}
void addLogs(List<BaseLog> logs){/*...*/}Now, you are trying to use this methods to add logs in one transaction from App.
Log[] logsArray = new Log[10];
List<Log> logsList = new ArrayList<Log>();addLogs(logsArray); // OK!
addLogs(logsList); // COMPILE ERROR!
The first usage of addLogs method is ok because BaseLog[] is a subtype of Log[]. The compile error was occurred because List<BaseLog> is NOT subtype of List<Log>. To fix this we need to use wildcards.
PECS. Wildcards
So if Generics are invariant, how we can follow polymorphism principle?
There is a special symbol “?” used in generics declaration which means unknown type in Java. More about wildcard, super and extends you can read in the documentation, but now let’s just consider the main idea of wildcards:
If A <: B, then
Generic<A> <: Generic<? extends A> <: Generic<? extends B> <: Generic<?>
AND
Generic<A> <: Generic<? super A> <: Generic<?>So now we can fix the compile error using this rule.
void addLogs(List<? extends BaseLog> logs){/*...*/}List<Log> logsList = new ArrayList<Log>();
addLogs(logsList); // OK!
We have fixed saving logs. So far so good. But what if we need to send them to some source.
public interface Source<T extends BaseLog> {
void accept(T log);
}Let’s use the same strategy.
public void sendToSource(Source<? extends BaseLog> source) {
for(BaseLog log : logs){
source.accept(log); // COMPILE ERROR!
}
}We can not just use Source<BaseLog> source as parameter because we would not be able to pass Source<Log> as parameter.
So why compile error? Parameter source could be one of the following
Source<? extends BaseLog> source1 = new SourceImpl<BaseLog>()
Source<? extends BaseLog> source2 = new SourceImpl<Log>()For all cases, the line source.accept(log); should be safe. To guarantee this compiler doesn’t allow using extends .
So what we should do? Here is PECS rule, which stands for
Producer
extendsand Consumersuper
You should use extends when you read the content of instance, and super if you are trying to consume instance with parameterized type.
In the method sendToSource(Source<? super BaseLog> source) variable source is “consumer” of elements. So the fix for sendToSource method would be Source<? super BaseLog>
public void sendToSource(Source<? super BaseLog> source) {
for(BaseLog log : logs){
source.accept(log);
}
}In the method addLogs(List<? extends BaseLog> logs) variable logs is “producer” of elements.
Recursive types
Recursive types is using mostly in builders.
For example, here is a source code of BaseBuilder for BaseLog class
public abstract class BaseBuilder<T extends BaseLog>{ protected String message;
public BaseBuilder<T> setMessage(String message){
this.message = message;
return this;
}
public abstract T build();
}
The implementation of this BaseBuilder for Log .
public class Builder extends BaseLog.BaseBuilder<Log>{
protected String source;
public Builder setSource(String source) {
this.source = source;
return this;
}
@Override
public Log build() {
return new Log(source, message);
}
}Now let’s use try to use Builder in the App
Log log = logBuilder.setMessage("started")
.setSource("source") // COMPILE ERROR!
.build();It is because method setMessage returns BaseBuilder that doesn’t have method setSource.
setMessage should return the current type of Builder. We can do this by using recursive types.
public abstract class BaseBuilder<T extends BaseLog, S extends BaseBuilder<T, S>>{ ... public S setMessage(String message){
this.message = message;
return self();
}
protected abstract S self();
public abstract T build();
}
To initialise S we should extend this class as following
public class Builder extends BaseLog.BaseBuilder<Log, Builder>{ ...
@Override
protected Builder self() {
return this;
} ...}
As you can see now S is the Builder itself. So the method setMessage will return the Builder, but not the BaseBuilder .
Consider this post as an tutorial for junior developers.
Code for this tutorial you can find here:
