slanted W3C logo

Day 26 — Substitutability

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

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

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

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

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:

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

Late Binding

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.

To Do For Next Lecture

Finish reading Chapter 9.