COMP 310

Covariant and Contravariant Generic Relationships in OO Systems

    Current Home  Java Resources  Eclipse Resources

Let us examine two different cases of covariant and contravariant generic relationships in object-oriented systems:

Case 1: Lists and Visitors

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:

  1. From IList.execute(), the condition on the IListAlgo tells us that TA ≥ TL  (TA is a superclass of or equal to TL)
  2. From 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.


Case 2:  Observable and Observers

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:

  1. Fromthe original Dispatcher-Observer relationship:  TO ≥ TD  (TO is a superclass of or equal to TD)
  2. From the desire to send out messsages:  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