Inheritance and Polymorphism
Reusing behavior through "is-a" relationships, and letting one method call do different things at runtime
So far each of our classes has stood alone. But many real-world modeling problems involve families of related things: shapes, employees, accounts, vehicles, events. Java lets one class extend another, inheriting its fields and methods. This is inheritance, and it's one of the most powerful — and most over-used — features of OOP.
"Is-a" relationships
Inheritance models an is-a relationship. A Dog is an Animal.
A SavingsAccount is a BankAccount. A Circle is a Shape.
If you cannot honestly say "X is a Y" in plain English, do not use inheritance to connect them.
The arrow with the open triangle means "extends." Circle and
Rectangle are both Shapes.
A first inheritance example
Run it. The single for loop prints information about three
different concrete shapes, even though the loop never says the words
Circle or Rectangle. That is polymorphism — the same call
(s.describe()) does different things depending on the actual
object behind the reference.
How polymorphism actually works
Each object remembers what class it is. When you write s.area(),
the JVM looks at the runtime class of s (not the declared type of
the variable) and dispatches to that class's area. This is
dynamic dispatch.
This is the mechanism that lets new shape classes be added later
without changing any of the code in Main. As long as a new class
extends Shape and overrides area, the loop works. That property
— "code written today keeps working as new types are added
tomorrow" — is the chief reason polymorphism exists.
abstract classes and abstract methods
In the example, Shape is abstract. That means:
- You cannot do
new Shape()directly. (What would its area be?) Shapemay declareabstractmethods (likearea()) that have no body and must be implemented by every non-abstract subclass.Shapemay also have concrete methods (likedescribe()) that use the abstract ones.
abstract is the language's way of saying: "this class is
incomplete on its own; it exists to be extended."
super and overriding
A subclass can call a superclass method via super.method(...),
and can call a superclass constructor via super(...) as the
first line of its own constructor.
super.describe() says "do whatever the parent class would
do, and let me add to it."
The @Override annotation isn't required, but always use it. It
asks the compiler to check that you really are overriding a method
in a parent class. If you misspell the method name (describ()
instead of describe()), the compiler will flag it instead of
silently treating it as a new unrelated method.
When inheritance is the wrong tool
Inheritance is a very strong coupling between two classes. The child knows about the parent's fields. The child can be broken by changes to the parent. The child cannot easily change parents later.
A good rule: prefer composition over inheritance. If Car
has-a Engine, that is composition (a field of type Engine), not
inheritance. Inherit only when the "is-a" relationship is genuine,
permanent, and substitutive — meaning anywhere a Shape is expected,
a Circle works correctly.
Common red flags that you should not be using inheritance:
- The subclass overrides almost every method to do something unrelated.
- The subclass needs to throw "unsupported operation" exceptions because the parent's contract doesn't fit.
- You wrote
extendsjust to reuse one method.
What does polymorphism let you do?
Convert one type into another at compile time
Allocate less memory for subclasses
Call the same method on different objects and have each respond in its own way, decided at runtime
Write a class without any methods
Which is the best signal that inheritance is appropriate between two classes?
They share a few fields
They sound related in English
Every instance of the child class genuinely is a legitimate instance of the parent and can be used wherever the parent is expected
The child wants to reuse one helper method from the parent
Why is the @Override annotation valuable?
It makes the override happen
It tells the compiler "this method is supposed to override a parent's method," so a typo or signature mismatch becomes a compile error instead of a silently-new method
It marks the method as final
It improves runtime performance
A small polymorphism challenge
Create a class hierarchy:
- An abstract class
Animalwith a methodString speak()(abstract) and a methodString greet()that returns"Hello, I say " + speak(). - A class
Dog extends Animalwhosespeak()returns"Woof!". - A class
Cat extends Animalwhosespeak()returns"Meow.".
Main.java is given and must print exactly:
Hello, I say Woof!
Hello, I say Meow.
Inheritance lets us reuse code along is-a lines. But Java has
another tool — interfaces — that captures can-do relationships
without the heavy coupling of extends. That is the next page.