This is the second post in a series of three about the Liskov Substitution Principle. The other posts focuses on LSP in combination with exceptions and contravariance so I will not dwell on those topics here.
The Liskov Substitution Principle LSP states that:
Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)
This boils down to some requirements on the type signature:
- Contravariance of method arguments in the subtype.
- Covariance of return types in the subtype.
- No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.
Let’s learn a little more about the Liskov Substitution Principle and covariance by investigating a design problem.
We run a small fruit store and have created a small class hierarchy that models different kinds of fruits.
We would like to group our fruits based on color. This is not a made up thing by the way, people actually do things like that.
In Java code this looks like:
And the client would use it like so:
Ok, so now group this list of apples instead.
But oh, here comes trouble, the compiler tells us that it expects a List<Fruit>
, not a List<Apple>
.
Or in the words of Gandalf:
But why you may ask? After all it looks totally safe to send in a List<Apple>
to method that expect a List<Fruit>
, this since an Apple
is a subtype of Fruit
and hence should support all the operations of Fruit
.
The operation used by groupFruitByColor
for example is color and we know that all subtypes of Fruit
(i.e. Apple
and Orange
) must implement this method.
One way of solving the problem would be to use that old copy/paste trick:
But this violates the DRY principle, so what to do, what to do?
So how can we convince the compiler that it’s safe to call groupFruitByColor
with a List<Apple>
? Well, we could use covariance!
Covariance is declared like this in Java:
And now the compiler is happy!
Better check with Gandalf…
Oh yeah, Gandalf is happy!
But what exactly did we do? Consider the following code:
It’s safe to assign an Apple
to a Fruit
variable since all operations of the supertype (Fruit
) must be supported by the subtype (Apple
).
But if we do the same to a List<Fruit>
and a List<Apple>
, the compiler complains.
The reason for this is that generic types: like List
, Optional
or Set
is by the default invariant in Java. By that we mean that List<Apple>
is not a subtype of List<Fruit>
. So if we want List<Apple>
to be a subtype of List<Fruit>
(i.e. to be covariant on Fruit
) we must override the default behaviour like this:
Even if fruitsCovariant
in the example above in reality is a List<Apple>
it is safe to use for the client to use since Apple
supports all operations of Fruit
. We have made fruitsCovariant
covariant on Fruit
.
So what is covariance? Well, it’s defined like this on wikipedia:
Within the type system of a programming language, a typing rule or a type constructor is:
- covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic
- contravariant if it reverses this ordering
In our case we preserve the ordering of types since: Fruit
is the supertype of Apple
and we have declared the List<Fruit>
(fruitsCovariant
) to be the supertype of all lists that contains Fruit
or any subtype of Fruit
(for example List<Apple>
). This means that we have fulfilled the definition of something that is covariant.
So why is not a List
covariant by default in Java, after all this looks like the desired behaviour?
Well, the thing is that Java relies on mutability to get the work done. Let’s investigate how a generic type would behave if it was covariant by default by looking on an array.
An array in Java is covariant by default:
Better check with Gandalf…
Oh yeah, Gandalf is still happy! But wait! Should he?
What happens if we do the following:
Well, the compiler don’t complain, it’s ok to insert an Orange
in an array of Fruit
, this since an Orange
is a subtype of Fruit
. But if we do the following:
We get an error by the compiler since an Orange
is not a subtype of Apple
, how does that add up? Well it don’t, the array in Java is broken and it’s very possible to make a fool of the compiler. The enemy is past the gate and we ends up with a run time exception: ArrayStoreException
.
But why did we end up in trouble? Well the reason was that the array is mutable, i.e. it can be modified after it is created. In this case the fact that arrayOfFruit
and arrayOfApple
references the same data structure, coupled with the fact that an array is mutable leads to disaster.
So have we introduced the same problem when making our List of Fruit
covariant? Well yes and no, we would have if the compiler let us add or update an element in the List, but does it?
No, we have not! The downside(?) is that we can only read from the covariant array which of course is a problem in a language like Java that relies on mutability to get the work done.
Let us investigate how another another generic type behaves in regard of covariance: the Supplier
. Also since it looks like I have captured your interest I will skip the GIF’s in the rest of this post, on the other hand feel free to skip the rest of the post if the GIF’s was what captured your interest.
The Supplier
interface was introduced in Java 8 when Java got support for lambdas and is used like so:
Since generic types are invariant the following is not possibe:
But is that a problem? Consider the following code:
printColorOfFruit
can be invoked like this:
But what if we would like to call printColorOfFruit
with a Supplier<Apple>
? After all this should be safe since an Apple
is a subtype of Fruit
and must support all operations of Fruit.
This is not possible. One way to fix the problem is to use copy/paste but we have already concluded that this is not optimal.
Could we use covariance again? Turn’s out that we can:
This works since by declaring the Supplier
as covariant in Fruit
we have turned the Supplier<Apple>
to a subtype of a Supplier<Fruit>
.
The Liskov Substitution Principle LSP states that:
Substitutability is a principle in object-oriented programming stating that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e. an object of type T may be substituted with any object of a subtype S) without altering any of the desirable properties of the program (correctness, task performed, etc.)
This boils down to some requirements on the type signature:
- Contravariance of method arguments in the subtype.
- Covariance of return types in the subtype.
- No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.
So why should the return types be covariant? Because we get a more flexible API (no copy/paste) and it is safe for the client to use.
In the code above it is always safe for the client to call supplierOfFruitCovariant
. The return type in this case is Fruit
which we have declared covariant as instructed by Liskov.
And why is it safe? It is safe since the client expects that a Fruit
is returned by supplierOfFruitCovariant. In the reality the client gets an Apple
, but that doesn’t matter since an Apple
is a subtype of Fruit
and hence safe to use for the client.
The same goes for our covariant list of fruit:
The client expects a List<Fruit>
, it could not care less if in reality it gets a List<Apple>
or a List<Orange>
(or a mix) since it only uses operations supported by Fruit
which in turn must be supported by the subtypes of Fruit
(Apple
and Orange
).
In the code above we are covariant in the return type of the getJoice
method in OrangeCovariant
which is a subtype of FruitCovariant
, i.e. a direct translation of “Covariance of return types in the subtype”. Once again it is quite safe for the for a client of FruitCovariant
to use getJoice
since it will get a Fruit
or a subtype of Fruit
in return.
The code examples are available at github.
If you enjoyed this post you may be interested in a related post about contravariance where we investigate how it can be used to get a more flexible API.