Covariance in Java return types (Part 2/3)
This post is the second part of a 3-part series. If you are not familiar with the term covariance, check out this article, which is the preceding 1st part of this series.
At the same time, you should also brush up on the definition of subtype:
If an object of class.Animal can be seamlessly changed with an object of class.Cat, it means (at least in theory) that Cat is a subclass of Animal.
a) Java comes with covariance on return types by default. This is useful if there are no input parameters of a method we want to override. We can make the method return something more specific, example: Animal:: Animal reproduce() gets overridden by Cat:: Cat reproduce().
If there are input parameters and we decide to override them, things get more complicated. Example: Cat:: cat mate(Cat) overloads Animal mate(Animal) instead of overriding it. This happens very implicitly and behind the scenes, making way for subtle bugs.
b) Generics solve the potential implicit overload instead of override problems by imposing stricter type limitations. This is usually the way to go. The benefits of generics in these particular examples are very obvious because it makes sense to have only one reproduce/mate method per derived class. Making the base class abstract, makes the type constraints even tighter, but it comes with some limitations.
Covariance pops up in java in a very subtle way. Consider the following take on cats, dogs, and animals in the evergreen subtyping examples below:
Note how ‘reproduce’ in the derived class.Cat [line 20] has a return type of Cat. This differs from the return type of ‘reproduce’ as defined in the base-class.Animal [line 8]. However, despite this difference in function definition, ‘reproduce’ in Cat is an override of ‘reproduce’ in Animal. There is no way to call ‘reproduce’ on a Cat and get an instance of the base-class.Animal. You will always get cats. The takeaway is that java has automatic covariance on function return parameters. HOWEVER, this can be a bit misleading when input parameters come in to play. Consider the function ‘mate’ [line 11] in the example below:
mate() is where the “fun” starts. Since java is covariant over return types, one would expect Cat mate(Cat) as defined in [line 28] in class.Cat to be an override of the Animal mate(Animal) in [line 11] in class.Animal.
This is, however, not the case. Java is covariant on return types only. Hence, the pesky input parameter makes our lives harder and less intuitive.
The function Cat mate(Animal) [line 23] in Cat.class indeed overrides the Animal mate(Animal) [line 11] in Animal.class. Overriding happens when the difference is only in the output parameter of the function ([line 23] overrides [line 11]).
When there is a difference in both a return parameter and input parameter in the derived function, it (the function in the derived class) is treated as a completely new function, independent of anything in the base class. Since this completely different function happens to share the same name (mate), we are dealing with an overload ([line 28] overloads [line 23]).
Cat mate(Animal) in Cat.class [line 23] is an override of Animal mate(Animal) in Animal.class.
Cat mate(Cat) in Cat.class [line 28] is an overload of Cat mate(Animal) in Cat.class [line 23] and has surprisingly nothing to do with Animal mate(Animal) in Animal.class [line 11].
The solution for this conundrum comes in the form of generics. If we want cats to mate exclusively with other cats and no other animals, we need to use generics. The example below illustrates how generic type parameters will ensure animals remain segregated by type when mating/reproducing:
In the generic implementation, there is a new method in the base class T mate(T other) [line 14].
Instead of specifying the type, we stick to generics. Since this is a bit too general, and we don’t want our animals to be able to mate with just any object, we need to put a constraint on T, that T has to be an animal [line 1]. Moreover, BaseGenericAnimal is now an abstract class.
In the derived class, we extend the base class and tell it that we are going to produce cats when mating with other cats:
Cat extends BaseGenericAnimal<Cat> [line 17].
In this implementation, there is only one override of the method ‘mate’ in the derived class. Moreover, no matter how hard we try, or how recklessly we pass parameters to the mate function, there is no chance of implicitly invoking a function from the base class. At least not with compilable code.
(Side note, this weirdly recursive type construct “Cat extends BaseGenericAnimal<Cat>” is known as F-bounded polymorphism)
Generics “kill two birds with one stone”:
1) They prevent the need for cats to explain why mating resulted in something that is not a cat,
2) They prevent buggy code and the need to explain covariance in the span of two articles to redeem oneself.
Know the tools you use and be aware of their drawbacks.