The cardinal principle of object-oriented subtyping:
A type SubA may only be a subtype of A if every instance of SubA can be safely substituted for any instance of A.
More specifically, we can say that, given a reference of type A which points to an object O:
O's type must be a subtype of A, which implies that it must be substitutable for A.
Therefore, O must accept all the messages that A can accept, and produce results that any client of A can accept. So:
O's methods must return a type at least as specific as the return type of A's corresponding methods.
O's methods must take parameters at least as general as the parameters of A's corresponding methods.
It is not always possible to prove statically (at compile time) that one object will be substitutable for another at run time. Various OO type systems use different typing rules to approximate the "substitutability principle" statically. We'll first discuss the "natural" way of thinking about subclassing, and then we'll discuss the type system used in Java and C++.
class Fruit { ... } class Apple extends Fruit { ... } class Orange extends Fruit { ... } class FruitPlant { Fruit produce() { ... } } class ApplePlant { Apple produce() { ... } } class FruitFly { void eat(Fruit f) { ... } } class AppleFly { void eat(Apple a) { ... } }
Given these method profiles, what should the subclassing relationships be? Consider: will an ApplePlant ever produce something that a FruitPlant could not? What about vice versa? Also, can an AppleEatingFly eat() something that cannot be eaten by a FruitEatingFly? What about vice versa?