Experiments in Java


Session O3: Interfaces and Polymorphism

You may recall from the discussion of inheritance that inheritance provides an elegant mechanism for reuse. In particular, by extending a class, you automatically reuse all the methods in that class with no additional effort. But are there other ways in which we may want to reuse code? Yes! Often, we would like to write general methods that will work on a variety of related objects. For example, we might design a printIndented method that should print any object or at least any printable object, indented by an appropriate number of spaces. Similarly, we might design a sort method that can sort any collection of elements or at least any collection of elements in which we can compare the individual elements.

In some languages, it is impossible or exceedingly difficult to design such methods. Fortunately, Java's inheritance mechanism and an accompanying feature called interfaces simplify the design of general methods. Together with polymorphism, interface and inheritance support generalized methods.

Polymorphism

What is polymorphism? It seems that there are hundreds of related definitions. You can think of polymorphism as the ability to use an object in a subclass in place of an object in a superclass, with the choice of methods to execute based on the actual class of the object.

For example, consider the SimpleDate class which we have used in the past. Here is a sample implementation of such a class.


/**
 * A very simple implementation of dates using Gregorian-style 
 * calendars (with year, month, and day).
 *
 * @author Samuel A. Rebelsky
 * @version 1.2 of September 1998
 */
public class SimpleDate {

  // +--------+--------------------------------------------------
  // | Fields |
  // +--------+

  /** The year. */
  protected int year;

  /** The month.  Use 1 for January, 2 for February, ...  */
  protected int month;

  /** The day in the month.  */
  protected int day;


  // +--------------+--------------------------------------------
  // | Constructors |
  // +--------------+

  /**
   * Build a new date with year, month, and day.  The month should be
   * between 1 and 12 and the day between 1 and the number of days in
   * the month.
   */
  public SimpleDate(int y, int m, int d) {
    this.year = y;
    this.month = m;
    this.day = d;
  } // SimpleDate(int,int,int)


  // +------------+----------------------------------------------
  // | Extractors |
  // +------------+

  /**
   * Get the year.
   */
  public int getYear() {
    return this.year;
  } // getYear()

  /**
   * Get the month.
   */
  public int getMonth() {
    return this.month;
  } // getMonth()

  /**
   * Get the day.
   */
  public int getDay() {
    return this.day;
  } // getDay()

  /**
   * Convert to a string (American format: MM/DD/YYYY)
   */
  public String toString() {
    return this.month + "/" + this.day + "/" + this.year;
  } // toString()

} // class SimpleDate


Suppose we want to subclass SimpleDate to provide more precise time information including not just the day, month, and year, but also minute and second on that day. Here is a class that does just that.


/**
 * Extended time/date information, including not just the day, but also the
 * time of day.  (Alternately, including not just the time, but the day on
 * which that time falls.)
 *
 * @author Samuel A. Rebelsky
 * @version 1.0 of January 1999
 */
public class SimpleTime extends SimpleDate {
  // +--------+---------------------------------------------------
  // | Fields |
  // +--------+
  
  /** The hour of the day.  */
  public int hour;
  
  /** The minute of the day.  */
  public int minute;
  
  
  // +--------------+---------------------------------------------
  // | Constructors |
  // +--------------+
  
  /**
   * Create a new time, specifying all the components.
   */
  public SimpleTime(int year, int month, int day, int hour, int minute) {
    // Set up the day without the time
    super(year,month,day);
    // Fill in the remaining information
    this.hour = hour;
    this.minute = minute;
  } // SimpleTime(int,int,int,int,int)
  
  /**
   * Create a new time, specifying only the day.  Uses 0:00 as the hour and
   * minute.
   */
  public SimpleTime(int year, int month, int day) {
    // Set up the day without the time
    super(year,month,day);
    // Fill in the remaining information
    this.hour = 0;
    this.minute = 0;
  } // SimpleTime(int,int,int)
  
  
  // +------------+-----------------------------------------------
  // | Extractors |
  // +------------+
  
  /**
   * Get the hour.
   */
  public int getHour() {
    return this.hour;
  } // getHour()
  
  /**
   * Get the minute.
   */
  public int getMinute() {
    return this.minute;
  } // getMinute()
  
  /**
   * Convert to a string.
   */
  public String toString() {
    if (this.minute < 10) {
      return this.hour + ":0" + this.minute + "," + super.toString();
    } // if the minute is a single digit
    else {
      return this.hour + ":" + this.minute + "," + super.toString();
    } // if the minute is multiple digits
  } // toString()
} // class SimpleTime


The calls to super(year,month,day) in the constructors are calls to the constructors of the superclass. As you may recall, the constructors in a subclass must begin with an explicit or implicit call to a constructor for the superclass.

The call to super.toString in the new toString says to use the toString method of the superclass, giving a string for the year, month, and day.

Now suppose a programmer wants to write a method to print a range of dates. The method might print the first date, a dash, and then the second date. The method might print the word ``from'', the first date, the word ``to'', and the second date. Here is some code that might do the latter. (You can find this code in DateHelper.java.)

  public void printRange(SimpleOutput out, 
                         SimpleDate first, 
                         SimpleDate second) {
    out.println("From " + first.toString() +
                " to " + second.toString());
  } // printRange(SimpleOutput,SimpleDate,SimpleDate)

What happens if we call this with two SimpleDates, as in

    helper.printRange(out, new SimpleDate(1964,6,17), 
                           new SimpleDate(1964,12,3));

As you might expect, it prints the range using just the dates, giving

From 6/17/1964 to 12/3/1964

What happens if we call this with two SimpleTimes, as in

    helper.printRange(out, new SimpleTime(1964,6,17), 
                           new SimpleTime(1964,12,3));

Even though printRange expects two SimpleDates, it ends up using the toString method of SimpleTime. The output will then be

From 0:00, 6/17/1964 to 0:00, 12/3/1964

In some object-oriented languages (such as C++), the default action would have been for printRange to use SimpleDate's toString method in both cases.

Polymorphism is very important. In fact, many computer scientists consider polymorphism one of the three key building blocks of object-oriented programming, the three being objects, inheritance, and polymorphism.

In Experiment O3.1 you will experiment with polymorphism.

Interfaces

As the asides in the introduction suggest, it would be nice if we could just say this method works on any object that provides these methods. For example, printIndented works on any object that provides getX and getY methods. It would seem that polymorphism, or something similar, should support such descriptions.

Let us consider the print method provided by the PointPrinter class. You may recall that that method is defined as

    out.println("(" + pt.getX() + "," + pt.getY() + ")");
    out.println("  distance from origin: " + 
                pt.distanceFromOrigin());

(While this method might be improved by using the toString method developed in a previous lab, note that by default Points do not include such a method.)

It seems that this method should be able to work on any class that provides getX, getY, and distanceFromOrigin methods. However, as you will see in Experiment O3.2, this is not the case.

Generalized methods

In particular, Java's type-checking mechanism, which verifies that when you call a method, the arguments match the types of the parameters to the method, will prevent you from using a NewPoint where a Point is called for. That is, even though NewPoint provides the same methods as Point the type-checking system is unable or unwilling to check this.

At the same time, Java needs some way to provide generalized methods, since the solution of rewrite the method with the same code but a different method header is not only inelegant, but also not always possible. As a simple example, consider the Plane or TextPlane classes used in the introductory laboratories. If we only had access to the files Plane.class or TextPlane.class and not to Plane.java or TextPlane.java, then there would be no way to plot NewPoints without first converting them to Points.

As you begin to use more code written by others, you will soon learn that a large number of professional programmers only distribute their compiled code and not their source code. For example, Microsoft will be happy to sell you a copy of the executable (i.e., compiled) versions of Word or Excel, but you will be unable to convince them to let you see the inner workings of the programs (i.e., their source code). At the same time, it is becoming increasingly likely that people will want to extend large programs with custom components, such as a new drawing tool to be used in a drawing program.

Fortunately, Java provides two mechanisms for writing generalized functions: inheritance and interfaces.

Generality through inheritance

The designers of Java decided that successful programming more formal mechanisms for indicating that a class has certain capabilities. Since every method provided by a class is automatically inherited or overridden by the subclass, inheritance provides a simple and elegant way for indicating just this. In particular, Java permits you to use an object from a subclass wherever an object for the superclass is called for. Hence, because Plane and TextPlane provide a plot(Point) methods, you can also plot ExtendedPoints. Similarly, because PointPrinter provides a print(Point) method, you can print ExtendedPoints.

You will investigate this type of reuse in Experiment O3.3.

Building generalized methods with Java's interfaces

As you've seen, there are two reasons to use inheritance. First, by extending another class, we can automatically provide all the methods of the class. Second, we can use a member of a subclass anywhere we can use a member of the superclass. We can take advantage of this second property to build generalized methods that act on a variety of objects. For example, Plane can plot anything that acts like a point.

What other generalized methods might we build? Typically, sorting and searching methods are written so that they can sort or search sequences of a wide variety of types. As long as you can compare two values, you can search or sort.

On a more practical level, we might want to improve SimpleOutput (or MyOutput, if you've done Lab X4) to print a wider variety of values, not just strings and numbers, but also other objects. For example, we might define a class, Printable, that includes a makePrintabale method used to convert the current object to a string. We can then say that it is possible to print any subclass of Printable using the following method

  /**
   * Print a printable object.
   */
  public void print(Printable printme) {
    this.print(printme.makePrintable());
  } // print(Printable)

Unfortunately, it is awkward to use Printable. In particular, what should the default makePrintable method (the one provided by Printable) do? More importantly, what happens if we want to subclass a nonprintable class, such as Point and also make a printable class. As mentioned in the previous laboratory session, Java does not permit you to subclass two classes.

Java provides an alternate mechanism for writing and using generalized functions. Rather than writing a superclass, you can instead write an interface. An interface definition looks surprisingly like a class definition except that

With inheritance, you indicate that a class extends another class. With interfaces, you indicate that a class implements another class. For example, we might indicate that a PrintablePoint implements the Printable interface with

public class PrintablePoint
  implements Printable

An interface is a contract between you and the compiler in which you assert that I have implemented all of the methods defined in the interface, and the compiler can trust your class to implement all of the methods declared in the interface.

You use interfaces similarly to the way you use superclasses except that,

For example, we can define a Printable interface as follows


/**
 * A simple interface used to describe classes that include a
 * toString method (and are therefore printable).
 *
 * @author Samuel A. Rebelsky
 * @version 1.0 of September 1998
 */
public interface Printable {
  /**
   * Convert the current object to a string.
   */
  public String makePrintable();
} // interface Printable


We can now declare a printable point (i.e., something that extends Point but is also printable) with

/**
 * A point that can be printed.
 */
public class PrintablePoint
  extends Point
  implements Printable 
{
} // class PrintablePoint

In Experiment O3.4, you will improve SimpleOutput by permitting it to print any class that implements Printable. In Experiment O3.5, you will perform other comparisons of interfaces and superclasses.


Copyright (c) 1998 Samuel A. Rebelsky. All rights reserved.

Source text last modified Tue Oct 26 13:29:31 1999.

This page generated on Tue Oct 26 15:38:05 1999 by Siteweaver.

Contact our webmaster at rebelsky@math.grin.edu