Let me give some background that will, I hope, change a few minds about polymorphism and how it should be used and when its use should be avoided. The essence of it is that too many of the examples are naive and incorrect. They may serve as metaphors, but are, in practice, unworkable. Let me try to show why and also show how polymorphism can be extremely useful - even essential in OO style programming.
One of the problems is that most OO languages permit things to be built in a way that they should not be built. That isn't special to OO, of course. You can write 20 page functions with if
and while
statements nested 15 deep in C, of course, though no one can understand or maintain the code. Likewise you can use concrete subclassing in a way that builds equally mis-formed software. One of the first examples of subclassing to appear in print was that a Circle class was a subclass of a Point class. It was done this way because it was easy to do, not because it was sensible. A circle isn't a special kind of point. But to build the Circle class from a Point class you could just add a single field for the radius and use the point coordinates for the center. But it makes no sense.
First, the Animal hierarchy is unsuitable teaching students about polymorphism unless you remember that the entire hierarchy consists of abstractions, not concrete "objects". There are no instantiations of class Mammal, or Vertebrate. Those are merely abstractions. There are Kangaroos, of course, but there are no subclasses of Kangaroo, unless Kangaroo itself is an abstraction.
Likewise Vehicle is a poor vehicle (heh heh) for teaching polymorphism as there are too many variations in the concrete things that carry that description. A bicycle, a golf cart, an automobile, and a tractor-trailer rig are all Vehicles. So is a Sherman Tank. They have very little in common other than mobility and the capability of carrying things. If you try to build a "hierarchy" of such things, the variations will overwhelm the commonalities. They are more "unlike" than they are "like", in fact. Just as for Animal.
The problem, as I see it, is that people try to give examples that are "too big". Top-level design with polymorphism. This is hard to manage and usually fails. In fact, one of my rules for building good software is that any concrete hierarchy that needs to add publicly visible elements in subclasses is fundamentally broken. Why is that?
Why it's broken
In building good software we need to manage complexity. Complexity is what kills us. It makes our programs buggy if not well managed and it also makes them hard to maintain. Suppose that you are at a point in a program in which you, the programmer, have built a structure that required ten thousand decisions to be made and the software didn't hide any of them from you at the current line you are writing. You need to take account of every decision that has been made. It is impossible. It is nearly impossible to maintain more than about seven independent items in your mind. Software is intended, actually, to hide such decisions. It is why that 15 level selection structure is impossible to maintain. Polymorphism is a way to manage complexity if used well, but since it can be used badly it can also add to the complexity burden. So, you need a way to use it well and to teach good usage.
For example, suppose that I have, in Java, a List of Vehicles. Suppose that Vehicle is a superclass and that every subclass of Vehicle has added different public features. The bicycle and tractor-trailer classes know about number of gears, but it is irrelevant for golf carts. All is well until we try to remove items from the List and operate on them. All the program knows about them is that they are vehicles unless either (a) we remember the most specific class of each item externally (mentally) or (b) we provide a way for the program to recapture the most specific class. Since (a) is essentially impossible, Java provides for (b), of course. But having to ask instanceof questions or do some sort of class based switch means that we have completely given up the advantages of polymorphism. We need to recapture the most specific thing when polymorphism is supposed to let us deal with general things. You are back to writing if
statements when they shouldn't be needed (if you had done a better job).
How to do better.
Suppose that instead of thinking of using polymorphism at a large scale, think of it at a much smaller scale. Don't try to get some polymorphic sense about vehicle overall, but think about how vehicles are actually built in the real world. I'll take bicycle as a simple example.
I have a lot of bicycles, actually. I have a nice one that is suitable for trail riding and for expeditions and even for the road. How is that. The bike I'm thinking of is composed of parts. It has a seat, wheels, gears, etc. I'll focus on just those. When I first purchased it the bike was set up primarily for moderately comfortable use on mild trails. It worked well for that. But the parts I mentioned are all replaceable. By changing the saddle I can make it better fit my anatomy. By changing the wheels I can make it much nicer on the road - both lighter and with a smoother ride. By changing the gears - even the number of gears I can make it more useful for serious rocky trails or for more modest prepared surfaces. This is polymorphism used well. I can easily swap parts and by giving it one part at one time and a different part at another time I can give it different behavior. It is the behavior that is polymorphic, not the interface. The two sets of wheels I have for this bike are interchangeable, but behave very differently. The saddles enable one use or another, for one rider or another. But the interface between the saddle and the rest of the bike is fixed. And I don't need to remember each time I switch gears whether I have the road wheels or the trail wheels on the bike. It "works" the same, but "behaves" differently without any decisions (if-this-then-that) on my part as I ride. The only time I need to make decisions is when I set up the bike for a ride (or an expedition) and then the bike itself "remembers" all of those decisions and behaves appropriately. I don't have to remember whether I'm on the "expedition bike" or the "fast road" bike. Nor do I have to recapture that knowledge at any point in my ride.
You can build software this way also. Build complex objects with composition - lots of part. Put the polymorphism in the parts. Each kind of part has an interface that doesn't need to be tailored to use (i.e. no added public features in subclasses). When the object of which these polymorphic things are a part needs to change its behavior, swap out one part for another. The overall object's interface didn't change, but its behavior does.
For example, in a Calculator, sometimes pressing the "five" key does one thing (accumulates into the current display value) and sometimes does something different (begins the next value in the computation after an operator key). You can use different objects to handle these two things. The calculator either has an accumulator object or an initiation object attached to the five key. The objects are swapped at appropriate times, but once the object is swapped the calculator itself has different behavior. But, the key idea, is that the accumulator object and the initiation object have exactly the same interface. This means that I, the programmer, don't need to remember which it is, the calculator remembers that, and I the programmer don't need to recapture the specific class of the object attached to the numeric key since I interact with both of them (as a programmer) in exactly the same way.
This is true polymorphism. Other ways of using it are very ad-hoc and require external complexity management that is better handled by the software itself.
This is how automobiles are built these days. You can buy most cars with different engines, different seats, different entertainment systems, etc. But each of those various engines presents the same interface to the rest of the automobile. Therefore they are swappable without modifying the chassis, and by swapping them they change the behavior of the object of which they are a part.
Let me add a simple rule of thumb for OO programmers who might benefit from the guidance.
Think of every addition you make to the public interface of a concrete subclass (even when implementing an interface) as a failure. It is sometimes necessary to do it and even the libraries show evidence of it, but think of every new public feature (in a concrete class) as a failure of design. You are building in the necessity to do ad-hoc decision making in the future. You can probably avoid that by rethinking the design. Every ad-hoc decision you don't need to make in the original build and especially in maintenance is a BIG WIN.