slanted W3C logo

Day 24 — Substitutability

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

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.

Assigning References

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.

Assigning References

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();

Passing Arguments to Methods

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.

Common Error I

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);

Common Error I

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();

Common Error II

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");

Polymorphism

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

Polymorphism

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.

Polymorphism

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.

Binding

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.

Binding

// 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).

Binding

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

Late Binding

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).

Late Binding

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.