
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.