Polymorphism using generics in java.

Taras Hor
Taras Hor
Sep 3, 2018 · 4 min read

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

Logs 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 <: Object

1. Arrays subtyping.

Arrays are covariant in Java. It means that

if the A is a subtype of the B, then the array of A is subtype of the array of B

if A <: B then A[] <: B[]

2. Generics subtyping.

Generics are invariant in Java. It means that

if the A is subtype of the B, then Generic<A> is NOT a subtype of the Generic<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 extends and Consumer super

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:

https://github.com/tarashor/generics-java

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade