slanted W3C logo

Day 27 — Casting, Abstract Classes, and Interfaces

Recap: Polymorphism

Polymorphism allows a subclass type to appear and behave like its superclass type. For example, a RewardCard can be used exactly like a CreditCard:

      CreditCard cc = new RewardCard(123, "Sally Sixpack");
      
      Date issued = cc.getIssueDate();
      Date expired = cc.getExpiryDate();
      double limit = cc.getLimit();
      // and so on, using any CreditCard method

Casting

In Java, you use a cast to convert one type to another type. For example, you can use a cast an integer type to a floating point type when dividing two numbers:

      double oneHalf = (double) 1 / 2;

Similarly, it is possible to convert the type of an object reference to another type. For example, you can convert a RewardCard reference to a CreditCard reference:

      RewardCard rc = new new RewardCard(123, "Sally Sixpack");
      CreditCard cc = (CreditCard) rc;

The cast in the example above is legal but unnecessary (because of substitutability). Such a cast is called an upcast because it casts up the inheritance hierarchy.

Downcasting

Sometimes, it is necessary to cast down the inheritance hierarchy using a downcast. For example, suppose you have a collection of credit cards that may or may not include reward cards. When you generate the monthly statement for each card, you want to print the reward points balance for the reward cards. A first (incorrect) attempt might look like:

      // assume gc is a GlobalCredit collection
      
      for (CreditCard cc = gc.getFirst();
           cc != null;
           cc = gc.getNext())
      {
         // print amount owing
         output.printf("Balance : $%.2f%n", cc.getBalance());
         
         // print reward points
         RewardCard rc = (RewardCard) cc;
         output.printf("Bonus points : %d%n", rc.getPointBalance());
      }

The above code compiles, but fails at runtime with an exception if one or more of the cards in the collection is not a RewardCard.

import java.io.PrintStream;
import java.util.Random;
import type.lib.CreditCard;
import type.lib.RewardCard;
import type.lib.GlobalCredit;

public class PolymorphismFail
{
   public static void main(String[] args)
   {
      PrintStream output = System.out;
      
      GlobalCredit gc = new GlobalCredit();
      
      // Randomly populate the GlobalCredit colllection
      // with credit and reward cards.
      final int NUM_CARDS = 10;
      Random rng = new Random();
      for (int i = 1; i <= NUM_CARDS; i++)
      {
         if (rng.nextBoolean())
         {
            // Ok, add takes a CreditCard
            gc.add(new CreditCard(i, "Some Name"));
         }
         else
         {
            // Ok, add takes a CreditCard and
            // RewardCard is substitutable for CreditCard
            gc.add(new RewardCard(i, "Another Name"));
         }
      }
      
      for (CreditCard cc = gc.getFirst();
           cc != null;
           cc = gc.getNext())
      {
         // print amount owing
         output.printf("Balance : $%.2f%n", cc.getBalance());
         
         // print reward points; the cast will fail if cc is a CreditCard
         RewardCard rc = (RewardCard) cc;
         output.printf("Bonus points : %d%n", rc.getPointBalance());
      }
   }
}

instanceof

What we need is a way to check if a CreditCard reference refers to an object that is or is not a RewardCard (or something substitutable for a RewardCard). In Java, you use the instanceof operator to check if a reference points to an object of a specified type:

      CreditCard cc1 = new CreditCard(123, "Sally Sixpack");
      CreditCard cc2 = new RewardCard(123, "Sally Sixpack");

      // does cc1 point to a CreditCard?
      boolean isCreditCard = cc1 instanceof CreditCard;
      output.println(isCreditCard);
      
      // does cc2 point to a CreditCard?
      isCreditCard = cc2 instanceof CreditCard;
      output.println(isCreditCard);
      
      // does cc1 point to a RewardCard?
      boolean isRewardCard = cc1 instanceof RewardCard;
      output.println(isRewardCard);
      
      // does cc2 point to a RewardCard?
      isRewardCard = cc2 instanceof RewardCard;
      output.println(isRewardCard);

The above code fragment prints:

true
true
false
true

x instanceof Y returns true if the reference variable x points to an object that is an instance of class Y or a subclass of Y.

import java.io.PrintStream;
import type.lib.CreditCard;
import type.lib.RewardCard;

public class DownCast
{
   public static void main(String[] args)
   {
      PrintStream output = System.out;
      
      CreditCard cc1 = new CreditCard(123, "Sally Sixpack");
      CreditCard cc2 = new RewardCard(123, "Sally Sixpack");

      // does cc1 point to a CreditCard?
      boolean isCreditCard = cc1 instanceof CreditCard;
      output.println(isCreditCard);
      
      // does cc2 point to a CreditCard?
      isCreditCard = cc2 instanceof CreditCard;
      output.println(isCreditCard);
      
      // does cc1 point to a RewardCard?
      boolean isRewardCard = cc1 instanceof RewardCard;
      output.println(isRewardCard);
      
      // does cc2 point to a RewardCard?
      isRewardCard = cc2 instanceof RewardCard;
      output.println(isRewardCard);
   }
}

instanceof

Note that the compiler only allows the client to use instanceof where the operation is sensible. It is a compile-time error if the reference has a type that is not in the same branch of the inheritance hierarchy as the type name:

      // all of these examples are compile-time errors
      boolean isInstanceOf;
      
      Date d = new Date();
      isInstanceOf = d instanceof String;
      
      Fraction f = new Fraction();
      isInstanceOf = f instanceof CreditCard;
      
      Scanner s = new Scanner(System.in);
      isInstanceOf = s instanceof Integer;

Downcasting and instanceof

If you need to downcast, you should use instanceof to prevent runtime errors.

      // assume gc is a GlobalCredit collection
      
      for (CreditCard cc = gc.getFirst();
           cc != null;
           cc = gc.getNext())
      {
         // print amount owing
         output.printf("Balance : $%.2f%n", cc.getBalance());
         
         // print reward points if cc is a RewardCard
         if (cc instanceof RewardCard)
         {
            RewardCard rc = (RewardCard) cc;
            output.printf("Bonus points : %d%n", rc.getPointBalance());
         }
      }
import java.io.PrintStream;
import java.util.Random;
import type.lib.CreditCard;
import type.lib.RewardCard;
import type.lib.GlobalCredit;

public class DownCast2
{
   public static void main(String[] args)
   {
      PrintStream output = System.out;
      
      GlobalCredit gc = new GlobalCredit();
      
      // Randomly populate the GlobalCredit colllection
      // with credit and reward cards.
      final int NUM_CARDS = 10;
      Random rng = new Random();
      for (int i = 1; i <= NUM_CARDS; i++)
      {
         if (rng.nextBoolean())
         {
            // Ok, add takes a CreditCard
            gc.add(new CreditCard(i, "Some Name"));
         }
         else
         {
            // Ok, add takes a CreditCard and
            // RewardCard is substitutable for CreditCard
            gc.add(new RewardCard(i, "Another Name"));
         }
      }
      
      for (CreditCard cc = gc.getFirst();
           cc != null;
           cc = gc.getNext())
      {
         // print amount owing
         output.printf("Balance : $%.2f%n", cc.getBalance());
         
         // print reward points if cc is a RewardCard
         if (cc instanceof RewardCard)
         {
            RewardCard rc = (RewardCard) cc;
            output.printf("Bonus points : %d%n", rc.getPointBalance());
         }
      }
   }
}

Java has two mechanisms for defining a type that has multiple different implementations.

The first mechanism is abstract classes. An abstract class serves as the superclass for the multiple implementations (the subclasses). The subclasses inherit from (extend) the abstract superclass.

The second mechanism is interfaces. Interfaces are different from inheritance. An interface defines the methods that a class must implement.

Both abstract classes and interfaces allow a client to write code based on a single API.

Abstract Classes

You can use inheritance to gather the common parts (attributes and methods) of two or more classes into a single class. Consider Figure 9.7 from the textbook:


In this example, Vehicle is the superclass of two classes Car and Bus. Vehicle defines all of the attributes and methods that are common to Cars and Buses.

Recognizing Abstract Classes

You can tell if a class is abstract from its API:

public abstract class Vehicle

An abstract class is usually incomplete in that it can declare what methods must exist (ie. drive) but it cannot implement the methods because the methods depend on details only the subclasses will know. Because abstract classes are usually incomplete, Java will not allow you to create an instance of an abstract class. It is a compile-time error to try to create a new Vehicle.

Using Abstract Classes

Although you cannot create an instance of an abstract class, you can create a instance of one of its subclasses (unless the subclass is also abstract). For example, you might be able to create a Car:

      Vehicle v = new Car();

Now, you can use v to call any Vehicle method. If Vehicle is well designed for its intended purpose, you may be able to write your entire program using only the Vehicle API!

Another common way of creating subclass instances is to use a static factory method; see Section 9.2.4 of the textbook for an example.

Interfaces

An interface is a group of one or more methods that a class promises to implement. For example, objects that have a natural order (numbers, strings, dates, etc) usually promise to implement the Comparable interface:

public interface Comparable
{
   int compareTo(T other);
}

The Comparable interface says that any class that implements the interface must provide a method named compareTo that returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

An interface is not a class; you cannot create an instance of an interface. Instead, you must find a class that implements the interface and create an instance of that class.

Classes that Implement Interfaces

You can tell if a class implements an interface based on its API:

public final class String
extends Object
implements Serializable, Comparable, CharSequence

Notice that unlike inheritance, a class may implement multiple interfaces.

Even though Comparable is not a class, it is still a valid type; thus, a String is-a Comparable. You can even create a reference to a Comparable object:

public class Interface
{
   public static void main(String[] args)
   {
      // compiles with a warning message
      
      Comparable s = "hello";
      System.out.println(s.compareTo("goodbye"));
   }
}

This is an odd example, but you will see many more examples in Chapter 10.

To Do For Next Lecture

Read Chapter 10.1.