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
Animal c = Cat();
Cat c with
MaineCoon c breaks type safety, because the superclass may provide a type of
Cat with different behaviors, such as
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
In the following example, you can assign a
MaineCoon list to
List<MaineCoon> is a subtype of
List<Cat> myCats = List<MaineCoon>();
What about going in the other direction? Can you assign an Animal list to a
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.