Covariance and contravariance
In object-oriented programming , covariance and contravariance mean whether an aspect (i.e. a type declaration) is similar to the direction of inheritance (covariant) or opposite to it (contravariant). If there is no change in the subclass compared to the upper class , this is called invariance .
The terms are based on the principle of replaceability : objects of the superclass must be replaceable by objects of one of its subclasses. This means, for example, that the methods of the subclass must accept at least the parameters that the superclass would also accept (contravariance). The methods of the subclass must also return values that are compatible with the superclass, i.e. are never of a more general type than the return type of the superclass (covariance).
Origin of the term
The terms contravariance and covariance are derived in object orientation from the fact that the types of the parameters under consideration relate to the inheritance hierarchy of replacement (covariant) or opposite to the inheritance hierarchy (contravariant).
Occurrence of variances
You can choose between co-, contra- and invariance at
- Methods
- Argument types (the types of parameters passed)
- Result types (the types of the return value)
- other signature extensions (e.g. exception types in the throws clause in Java )
- generic class parameters
distinguish.
The substitution principle results in the following possible occurrences of variances in the inheritance hierarchy of object-oriented programming:
Contravariance | Input parameters |
Covariance | Return value and exceptions |
Invariance | Input and output parameters |
Covariance, contravariance and invariance
Covariance means that the type hierarchy has the same direction as the inheritance hierarchy of the classes to be considered. If you want to adapt an inherited method, the adaptation is covariant if the type of a method parameter in the superclass is a supertype of the parameter type of this method in the subclass.
If the type hierarchy runs in the opposite direction to the inheritance hierarchy of the classes to be considered, one speaks of contravariance. If the types in the upper and lower classes cannot be changed, one speaks of invariance.
In object-oriented modeling it is often desirable that the input parameters of methods are also covariant. However, this violates the substitution principle. The overloading is handled differently in this case by the various programming languages.
Example based on program code
Basically, in programming languages like C ++ and C #, variables and parameters are contravariant, while method returns are covariant. Java, on the other hand, requires the novariance of the method parameters and variables, whereby the return parameter must be covariant:
Contravariance | Covariance | Invariance |
---|---|---|
public abstract class Animal
{
public abstract string Name { get; }
}
public class Giraffe : Animal
{
public Giraffe(string name)
{
Name = name;
}
public string Name { get; private set; }
}
public string GetNameFromAnimal(Animal animal)
{
return animal.Name;
}
[Test]
public void Contravariance()
{
var herby = new Giraffe("Herby");
// kontravariante Umwandlung von Giraffe nach Animal
var name = GetNameFromAnimal(herby);
Assert.AreEqual("Herby", name);
}
|
public abstract class Animal
{
public abstract string Name { get; }
}
public class Giraffe : Animal
{
public Giraffe(string name)
{
Name = name;
}
public string Name { get; private set; }
}
public string GetNameFromGiraffe(Giraffe animal)
{
return animal.Name;
}
[Test]
public void Covariance()
{
var herby = new Giraffe("Herby");
// kovariante Umwandlung des Rückgabewerts von String nach Object
object name = GetNameFromGiraffe(herby);
Assert.AreEqual((object)"Herby", name);
}
|
public abstract class Animal
{
public abstract string Name { get; }
}
public class Giraffe : Animal
{
public Giraffe(string name)
{
Name = name;
}
public string Name { get; private set; }
}
public string GetNameFromGiraffe(Giraffe animal)
{
return animal.Name;
}
[Test]
public void Invariance()
{
var herby = new Giraffe("Herby");
// keine Umwandlung der Datentypen
string name = GetNameFromGiraffe(herby);
Assert.AreEqual("Herby", name);
}
|
Example based on illustrations
The following explains when type safety is guaranteed when you want to replace one function with another. This can then be transferred to methods in object orientation if methods are replaced by objects according to Liskov's principle of substitution.
Be and functions that have the following signature , for example :
- , where and , and
- , where and .
As you can see, it is a superset of , but a subset of . If the function is used instead of , then the input type C is called contravariant, the output type D covariant. In the example, the replacement can take place without a type violation, since the input of covers the entire range of the input of . It also provides results that do not exceed the value range of .
Correctness of contravariance and covariance
The UML notation is used as a model to represent the inheritance hierarchy:
Kontravarianz Kovarianz Invarianz ┌─────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ T │ │ ClassA │ │ ClassA │ │ ClassA │ ├─────────┤ ├───────────────┤ ├───────────────┤ ├───────────────┤ │ │ │ │ │ │ │ │ ├─────────┤ ├───────────────┤ ├───────────────┤ ├───────────────┤ │ │ │ method(t':T') │ │ method():T │ │ method(t :T&) │ └─────────┘ └───────────────┘ └───────────────┘ └───────────────┘ ↑ ↑ ↑ ↑ ┌─────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ T' │ │ ClassB │ │ ClassB │ │ ClassB │ ├─────────┤ ├───────────────┤ ├───────────────┤ ├───────────────┤ │ │ │ │ │ │ │ │ ├─────────┤ ├───────────────┤ ├───────────────┤ ├───────────────┤ │ │ │ method(t :T ) │ │ method():T' │ │ method(t :T&) │ └─────────┘ └───────────────┘ └───────────────┘ └───────────────┘
Contravariance : The substitution principle is adhered to, because you can method (t: T) of Division ClassB so use as if it were the method of upper class ClassA .
Check: You can transfer a variable of a more special type T ' to method (t: T) , because T' contains all information that is also in T due to inheritance .
Covariance : The substitution principle is adhered to, because method (): T 'of the subclass ClassB can be used as if it were the method of the superclass ClassA .
Check: The return value of the method from ClassB is T ' . You can transfer this value to a variable declared of type T , because T ' has all the information that is also in T due to inheritance .
Type safety in methods
Due to the properties of the substitution principle, static type safety is guaranteed if the argument types are contravariant and the result types are covariant.
Type-uncertain covariance
The covariance of the method parameters, which is often desirable in object-oriented modeling, is supported in many programming languages despite the resulting type uncertainty.
An example of the type uncertainty of covariate method parameters can be found in the following classes Person
and Arzt
, and their specializations Kind
and Kinderarzt
. The parameter of the method untersuche
in the class Kinderarzt
is a specialization of the parameter of the same method of Arzt
and therefore covariant .
Type-uncertain covariance - general
┌─────────┐ ┌───────────────┐ │ T │ │ ClassA │ ├─────────┤ ├───────────────┤ │ │ │ │ ├─────────┤ ├───────────────┤ │ │ │ method(t :T ) │ └─────────┘ └───────────────┘ ↑ ↑ ┌─────────┐ ┌───────────────┐ │ T' │ │ ClassB │ ├─────────┤ ├───────────────┤ │ │ │ │ ├─────────┤ ├───────────────┤ │ │ │ method(t':T') │ └─────────┘ └───────────────┘ |
Example of type-uncertain covariance
┌────────────────┐ ┌───────────────────────┐ │ Person │ │ Arzt │ ├────────────────┤ ├───────────────────────┤ │ │ │ │ ├────────────────┤ ├───────────────────────┤ │ stillHalten() │ │ untersuche(p: Person) │ └────────────────┘ └───────────────────────┘ ↑ ↑ ┌────────────────┐ ┌───────────────────────┐ │ Kind │ │ Kinderarzt │ ├────────────────┤ ├───────────────────────┤ │ │ │ │ ├────────────────┤ ├───────────────────────┤ │ tapferSein() │ │ untersuche(k: Kind) │ └────────────────┘ └───────────────────────┘ |
The implementation of the example in Java looks like this: | A program using the classes could look like this: | The output is then: |
---|---|---|
public class Person {
protected String name;
public String getName() { return name; }
public Person(final String n) { name = n; }
public void stillHalten() {
System.out.println(name + " hält still");
}
}
public class Kind extends Person {
boolean tapfer = false;
public Kind(final String n) {super(n); }
public void stillHalten() {
if(tapfer)
System.out.println(name + " hält still");
else
System.out.println(name + " sagt AUA und wehrt sich");
}
public void tapferSein() {
tapfer = true;
System.out.println(name + " ist tapfer");
}
}
public class Arzt extends Person {
public Arzt(final String n) { super(n); }
public void untersuche(Person person) {
System.out.println(name + " untersucht " + person.getName());
person.stillHalten();
}
}
public class Kinderarzt extends Arzt {
public Kinderarzt(final String n) { super(n); }
public void untersuche(Kind kind) {
System.out.println(name + " untersucht Kind " + kind.getName());
kind.tapferSein();
kind.stillHalten();
}
}
|
public class Main {
public static void main(String[] args) {
Arzt arzt = new Kinderarzt("Dr. Meier");
Person person = new Person("Frau Müller");
arzt.untersuche(person);
Kind kind = new Kind("kleine Susi");
arzt.untersuche(kind);
// und jetzt RICHTIG
Kinderarzt kinderarzt = new Kinderarzt("Dr. Schulze");
kinderarzt.untersuche(person);
kinderarzt.untersuche(kind);
}
}
|
Dr. Meier untersucht Frau Müller
Frau Müller hält still
Dr. Meier untersucht kleine Susi
kleine Susi sagt AUA und wehrt sich
Dr. Schulze untersucht Frau Müller
Frau Müller hält still
Dr. Schulze untersucht Kind kleine Susi
kleine Susi ist tapfer
kleine Susi hält still
|
It is important that the object arzt
must be correctly declared because a method is not overwritten here, but rather overloaded, and the process of overloading is tied to the static type of the object. You can see the result when you compare the expenses: Dr. Meier cannot examine children, Dr. Schulze, however, does.
The example works correctly in Java: The method untersuche
of Arzt
is Kinderarzt
not overwritten, but simply overloaded due to the different parameters, which means that the correct method is called in each case. When Arzt
untersuche
called, the method is always called there; however, when Kinderarzt
untersuche
called, it is called once untersuche
at Arzt
and once at , depending on the type Kinderarzt
. According to the language definition of Java, a method that is to be overwritten must have the same signature (in Java consisting of parameters + possible exceptions).
The same example can also be coded in Python, but note that parameters are not typed. The code would look like this:
#!/usr/bin/env python
class Person:
def __init__(self,name):
self.name = name
def stillHalten(self):
print(self.name, " hält still")
class Arzt(Person):
def __init__(self,name):
super().__init__(name)
def untersuche(self,person):
print(self.name, " untersucht ", person.name)
person.stillHalten()
class Kind(Person):
def __init__(self,name):
super().__init__(name)
self.tapfer = False
def tapferSein(self):
self.tapfer = True
print(self.name, " ist jetzt tapfer")
def stillHalten(self):
if self.tapfer:
print(self.name, " hält still")
else:
print(self.name, " sagt AUA und wehrt sich")
class Kinderarzt(Arzt):
def __init__(self,name):
super().__init__(name)
def untersuche(self,person):
print(self.name, " untersucht ", person.name)
if isinstance(person,Kind):
person.tapferSein()
person.stillHalten()
if __name__ == "__main__":
frMüller = Person("Frau Müller")
drMeier = Arzt("Dr. Meier")
drMeier.untersuche(frMüller)
kleineSusi = Kind("kleine Susi")
drMeier.untersuche(kleineSusi)
drSchulze = Kinderarzt("Dr. Schulze")
drSchulze.untersuche(frMüller)
drSchulze.untersuche(kleineSusi)
Covariance on arrays
With array data types, covariance can cause a problem in languages like C ++, Java, and C #, as they internally retain the data type even after the conversion:
Java | C # |
---|---|
@Test (expected = ArrayStoreException.class)
public void ArrayCovariance()
{
Giraffe[] giraffen = new Giraffe[10];
Schlange alice = new Schlange("Alice");
// Kovarianz (Typumwandlung in Vererbungsrichtung)
Tier[] tiere = giraffen;
// führt zur Laufzeit zu einer Ausnahme,
// da das Array intern vom Typ Giraffe ist
tiere[0] = alice;
}
|
[Test, ExpectedException(typeof(ArrayTypeMismatchException))]
public void ArrayCovariance()
{
var giraffen = new Giraffe[10];
var alice = new Schlange("Alice");
// Kovarianz
Tier[] tiere = giraffen;
// Ausnahme zur Laufzeit
tiere[0] = alice;
}
|
To avoid such runtime errors, generic data types can be used that do not offer any modifying methods. The interface IEnumerable<T>
implemented by the array data type, among other things , is often used in C # . Since a IEnumerable<Tier>
cannot be changed, z. For example, a new instance can be created from LINQ using extension methods to accommodate the element .
alice
[Test]
public void ArrayCovariance()
{
var giraffen = new Giraffe[10];
var alice = new Schlange("Alice");
IEnumerable<Tier> tiere = new Tier[]{ alice }
.Concat(giraffen.Skip(1).Take(9));
Assert.Contains(alice, tiere);
}