Substituting types
When you override a method, you are replacing something of one type (in the old method) with something that might have a new type (in the new method). Similarly, when you pass an argument to a function, you are replacing something that has one type (a parameter with a declared type) with something that has another type (the actual argument). When can you replace something that has one type with something that has a subtype or a supertype?
When substituting types, it helps to think in terms of consumers and producers. A consumer absorbs a type and a producer generates a type.
You can replace a consumer’s type with a supertype and a producer’s type with a subtype.
Let’s look at examples of simple type assignment and assignment with generic types.
Simple type assignment
When assigning objects to objects, when can you replace a type with a different type? The answer depends on whether the object is a consumer or a producer. Consider the following type hierarchy:
Consider the following simple assignment where Cat c is a consumer and Cat()
is a producer:
Cat c = Cat();
In a consuming position, it’s safe to replace something that consumes a specific type (Cat
) with something that consumes anything (Animal
), so replacing Cat c
with Animal c
is allowed, because Animal
is a supertype of Cat
.
Animal c = Cat();
But replacing Cat c
with MaineCoon c
breaks type safety, because the superclass may provide a type of Cat
with different behaviors, such as Lion
:
MaineCoon c = Cat();
In a producing position, it’s safe to replace something that produces a type (Cat
) with a more specific type (MaineCoon
). So, the following is allowed:
Cat c = MaineCoon();
Generic type assignment
Are the rules the same for generic types? Yes. Consider the hierarchy of lists of animals—a List of Cat is a subtype of a List of Animal, and a supertype of a List of MaineCoon:
In the following example, you can assign a MaineCoon
list to myCats
because List<MaineCoon>
is a subtype of List<Cat>
:
List<Cat> myCats = List<MaineCoon>();
What about going in the other direction? Can you assign an Animal list to a List<Cat>
?
List<Cat> myCats = List<Animal>();
This assignment passes static analysis, but it creates an implicit cast. It is equivalent to:
List<Cat> myCats = List<Animal>() as List<Cat>;
The code may fail at runtime. You can disallow implicit casts by specifying implicit-casts: false
in the analysis options file.
When overriding a method, the producer and consumer rules still apply. For example:
For a consumer (such as the chase(Animal)
method), you can replace the parameter type with a supertype. For a producer (such as the parent
getter method), you can replace the return type with a subtype.