COMP 310 |
Covariant and Contravariant Generic Relationships in OO Systems |
Let us examine two different cases of covariant and contravariant generic relationships in object-oriented systems:
Consider the following scenario where we have a generic list, IList
,
and its associated visitor, IListAlgo
:
public interface IList<TL> { <R, P> R execute(IListAlgo<? super TL, R, P> algo, P... params); } // IMTList and INEList are subclasses of IList that represent the empty and non-empty lists respectively. public interfce IMTListt<TL> extends IList<TL> { } public interfce INEListt<TL> extends IList<TL> { // methods elided }
For the list, the generic parameter <TL>
refers to the type of
elements that the list holds. In the execute()
method,
we see the list's contravariant relationship with its visitor: the list
can accept any visitor which is able to process a list holding superclasses of
what this list holds.
public interface IListAlgo<TA, R, P> { R emptyCase(IMTList<? extends TA> host, P... params); R nonEmptyCase(INEList<? extends TA> host, P... params); }
For the visitor, the generic parameter <TA>
refers to the type
of element that the visitor algorithm is able to process. In the
visitor case methods, we see the visitor's covariant relationshop with its host
list: the list can process any list that holds subclasses of what the
algorithm is able to process.
The co-variant and contravariant relationships here tell us the relationship
between the <TL>
and <TA>
generic parameters in the
list+visitor system:
IList.execute()
, the condition on the IListAlgo
tells us that TA ≥ TL
(TA
is a
superclass of or equal to TL
) IListAlgo.xxCase()
, the condition on the IList
tells us that TL ≤ TA
(TL
is a subclass of or equal to
TA
)Both observations are consistent with each other; they are just different
statements of the same relative relationship between TL
and TA
,
taken from different viewpoints. The covariant and contravariant
relationships between the list and its associated vsiitor algorithms are just
two sides of the same coin.
In the list+visitor system, the list and visitor only impose lower- or upper-bounded generic type ranges respectively on the other entity.
For a more pictorally-oriented discussion of this topic, please see Covariance and Contravariance of Hosts and Visitors.
Let's consider the more complicated case embodied by the Observer-Observable
design pattern. For simplicity here, we will ignore the
state-change aspects of the Observable, reducing it to something we will refer
to as a "Dispatcher
", which will always and immediately update all
its Observers
(i.e call Observer.update()
) upon command.
In the implementation discussed below, the dispatcher is passed to the observer
during updating so that the observer is then able to send its own messages back
out through the dispatcher to the other observers.
public interface Observer<TO> { public void update(Dispatcher<? extends TO> disp, TO msg); }
The Observer
's generic parameter
<TO>
is the type of the message that the Observer
is able to process. In its update()
method, we
see that the Observer
is able to utilize any Dispatcher
that is capable of sending subclasses of this Observer
's
message type. This is based on the presumption that the
Observer
would be free to construct subclass instances of TO
to pass along to the given Dispatcher
, which is what we
would expect if TO
represented an abstract message type.
public interface Dispatcher<TD> { public void updateAll(TD msg); public void addObserver(Observer<? super TD> obs); }
The Dispatcher
's generic parameter
<TD>
is the type of the message that is sent (via its
updateAll()
method) to all the Observers
it holds.
The Dispatcher
's addObserver()
method shows us
the dispatcher's contravariant relationship with its observers: the dispatcher
can send messages to any observer that is capable of processing a superclass of
the message being sent.
The above two conditions are not in conflict; they are just statements of the same relative relationship from two different viewpoints.
The above conditions can be summarized as implying that
TO ≥ TD
(TO
is a
superclass of or equal to TD
)
However, let's consider the following scenario:
What if the Observers want to send messages to other Observers?
In this situation, what are the constraints on the
Dispatcher
?
The Observer
wants to
send a message out, knowing that the other Observers
can all handle a message of type TO
.
This implies that the Dispatcher
needs to be able
to handle any message that is a super-type
of TO
.
Putting together the original constraints on TO
and TD
from above as well as that implied by the the desire of an Observer
to send out messages to other Observers, we can thus identify these relationships between the
TO
and
TD
generic parameters:
Dispatcher-Observer
relationship: TO ≥ TD
(TO
is a
superclass of or equal to TD
)TD ≥ TO
(TD
is a
superclass of or equal to TO
)Unfortunately, the above relationships between the TO
and TD
generic parameters are inconsistent except in one situation: TD
= TO
This says that when we only look at the dispatcher or the observer
without the notion of sending out messages from Observers
,
we can see the self-consistent covariant/contravariant relationship between them.
However, when we add the ability of observers to send messages to each other, there is no consistent, lower- or
upper-bounded generic type range that can be applied. The only
solution is type equality:
public interface Observer<T> { public void update(Dispatcher<T> disp, T msg); } public interface Dispatcher<T> { public void updateAll(T msg); public void addObserver(Observer<T> obs); }
Unfortunately, the as much as we would like to be able to leave the less
restricted bounded type definitions for the Dispatcher
and
Observer
, the Java type engine is unable to infer that when sending
messages is allowed (essentially that the Dispatcher
is passed to
the Observer
during the notify/update process), that the only
solution for the generic types is equality. Thus we are forced to
explicitly define the classes with their generic types as equal.
© 2017 by Stephen Wong