Inheritance models the is-a relationship: a subclass (child class) object is-a superclass (parent class) object.
It makes sense then that a subclass object is substitutable for a superclass object.
Substitutability in Java means:
A client can use a subclass reference anywhere a superclass reference is asked for.
Consider the real life example of trying to purchase an item using a credit card. It does not matter what kind of credit card you use (Visa, MasterCard, Amex) or whether the card has some sort of reward scheme associated with it; the merchant simply charges the card to perform the transaction.
A client can use a subclass reference anywhere a superclass reference is asked for.
One example of substitutability is assigning a subclass reference to a superclass reference variable:
CreditCard cc = new RewardCard(123456, "Lisa Simpson");
In the above example a CreditCard
reference
variable cc
actually points to a RewardCard
object.
A RewardCard
is-a
CreditCard
so Java allows a RewardCard
reference to be stored in a CreditCard
reference variable
without requiring a cast.
Of course, we can invoke any CreditCard
method using cc
:
CreditCard cc = new RewardCard(123456, "Lisa Simpson"); cc.charge(10.0); double amtOwed = cc.getBalance();
A client can use a subclass reference anywhere a superclass reference is asked for.
A second example of substitutability is passing a subclass reference to a method expecting a superclass reference:
GlobalCredit gc = new GlobalCredit(); RewardCard rc = new RewardCard(123456, "Lisa Simpson"); gc.add(rc);
GlobalCredit
models a collection of credit cards
so its add
method takes a CreditCard
parameter; a RewardCard
is-a CreditCard
so Java allows a RewardCard
reference variable to
be passed to the add
method.
A common error occurs when the programmer forgets that inheritance is a one-way relationship. A subclass is substitutable for its superclass, but the opposite is not true.
It is legal (and common) to store a subclass reference in a superclass reference variable:
CreditCard cc = new RewardCard(123456, "Lisa Simpson");
but it is illegal to try to use a subclass method via a superclass reference variable, even if you know that the reference really is a subclass reference:
// ok, charge is a CreditCard method
cc.charge(10);
// Try to redeem 1 reward point:
// compilation error, redeem is a RewardCard method
// and cc is not a RewardCard
cc.redeem(1);
Another example:
// empty collection of CreditCard
GlobalCredit gc = new GlobalCredit();
// put a RewardCard into the collection
gc.add(new RewardCard(123456, "Lisa Simpson");
// get the reward card from the collection as
// a CreditCard
CreditCard cc = gc.get("123456-6");
// try to get the reward points balance
// compilation error: cc is not a RewardCard
int points = cc.getPointBalance();
The same error of mistaking a superclass reference for a subclass reference appears when a method returns a reference to a superclass object:
// empty collection of CreditCard
GlobalCredit gc = new GlobalCredit();
// put a RewardCard into the collection
gc.add(new RewardCard(123456, "Lisa Simpson");
// try to get the reward card from the collection
// compilation error: get returns a CreditCard
// and a CreditCard is not a RewardCard
RewardCard rc = gc.get("123456-6");
Suppose you create a RewardCard
object and store a reference to it
in a CreditCard
reference variable.
What happens when you charge the
card?
Recall that RewardCard
overrides
the charge
method so
that it can assign the correct number of bonus points to the
point balance of the card.
RewardCard rc = new RewardCard(123456, "Lisa Simpson"); rc.charge(100); output.println(rc.getPointBalance()); // should print 5
What happens if you use a CreditCard
reference instead?
CreditCard cc = new RewardCard(123456, "Lisa Simpson"); cc.charge(100); output.println(cc.getPointBalance());
The CreditCard
reference variable cc
was used to invoke the
charge
method, so you might think that the
CreditCard
version of charge
would be invoked
(ie. no bonus points would be awarded). This would be
very surprising because cc
actually points to
a RewardCard
object.
In fact, the RewardCard
version of
charge
is invoked
and the bonus points are awarded.
In object-oriented programming, polymorphism is the ability of a
type (such as RewardCard
) to appear and be used like
another type (such as CreditCard
).
A key feature of polymorphism is that different types
(RewardCard
and CreditCard
) can define
their own behavior when a method is invoked. In Java, a subclass
defines its own specialized behavior by overriding a superclass
method.
In the example on the previous slide, charging the credit
card caused bonus points to be awarded
because RewardCard
overrides the charge
method in CreditCard
.
How does the Java language resolve which version of a method to invoke in the presence of polymorphism? Consider the following fragment of code:
// Random generates random numbers and booleans Random rand = new Random(); // Declare cc to be of type CreditCard CreditCard cc = null; // randomly create a CreditCard or RewardCard if (rand.nextBoolean()) { cc = new CreditCard(123456, "Lisa Simpson"); } else { cc = new RewardCard(123456, "Lisa Simpson"); }
Notice that cc
is randomly chosen to
be either a CreditCard
or a
RewardCard
.
// continued from previous slide cc.charge(100.0);
As we have already seen, the statement cc.charge(100);
will result in the RewardCard
version of charge
if cc
is in fact a reference to a RewardCard
object even though the declared type of cc
is
CreditCard
.
Because the actual type of the object pointed to by
cc
is chosen randomly at run time,
there is no way for the compiler
to deduce which version of charge
to invoke.
All the compiler can do is determine that charge
is the appropriate CreditCard
method to invoke.
This is called early binding (from Chapter 3).
When the code is run, the virtual machine determines the
actual type referred to by cc
. If cc
refers to a RewardCard
then the RewardCard
version of charge
is invoked.
Determining if a subclass version of a method needs to be invoked is called late binding.
Early binding | Late binding | |
performed at: | compile time | run time |
searches: | declared class | actual class |
Consider the following example
where card1
is used
to invoke a method named isSimilar
.
CreditCard card1 = new RewardCard(123, "Lisa Simpson");
CreditCard card2 = new RewardCard(456, "Bart Simpson");
boolean similar = card1.isSimilar(card2);
The compiler performs
early binding by looking at the declared type of
card1
(CreditCard
)
and the declared type of
card2
(CreditCard
)
to determine if the code is legal.
At run time,
late binding looks at the actual type of card1
(RewardCard
)
to determine if an overridden method needs to be invoked.
For the purposes of late binding, only the declared type
of card2
is considered
(ie. method arguments do not behave polymorphically).
CreditCard card1 = new RewardCard(123, "Lisa Simpson"); CreditCard card2 = new RewardCard(456, "Bart Simpson"); card1.charge(500); card1.pay(500); boolean similar = card1.isSimilar(card2);
Late binding looks at the actual type of card1
,
which is RewardCard
. RewardCard
has 2 methods named isSimilar
:
boolean isSimilar(CreditCard other) boolean isSimilar(RewardCard other)
It is tempting, but incorrect, to think that the second version of
isSimilar
is chosen by late binding because the actual type of
card2
is RewardCard
.
Remember that method arguments do not behave polymorphically;
only the declared type matters for the arguments.
The declared type of
card2
is CreditCard
so the first
version of isSimilar
is chosen by late binding.