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
:
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);
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: getFirst returns a CreditCard // and a CreditCard is not a RewardCard RewardCard rc = gc.getFirst(); // get the reward card from the collection as // a CreditCard CreditCard cc = gc.getFirst(); // try to get the reward points balance // compilation error: cc is not a RewardCard int points = cc.getPointBalance();
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"); CreditCard cc = rc; cc.charge(100); output.println(rc.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"); } cc.charge(100);
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, 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
(see Day08 slides).
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:
// assume that card1 and card2 are declared as // type CreditCard boolean similar = card1.isSimilar(card2);
The reference variable card1
is used
to invoke a method named isSimilar
.
Late binding looks at the actual class of card1
because it was the reference used to invoke the method.
For the purposes of binding, only the declared type
of card2
is considered.
Consider the following example:
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
.
Late binding only looks at the actual type of the reference
used to invoke the method; the types of the parameters are their
declared types. In this example, the declared type of
card2
is CreditCard
so the first
version of isSimilar
is chosen by late binding.
Finish reading Chapter 9.