Experiments in Java


Session O3: Interfaces and Polymorphism

In a previous laboratory, you examined Java's mechanisms for inheritance in which a subclass automatically inherits all the methods provide by its superclass. In this laboratory you will investigate a related mechanism, Java's interfaces. You will also learn about polymorphism, one of the key attributes of object-oriented programming. The goals of this laboratory are to

Your instructor will tell you which of the experiments to perform.

Prerequisite skills:

Required files:


Discussion

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.


Experiments

Name: ________________
ID:_______________

Experiment O3.1: Polymorphism

Required files:

Before you begin, make copies of DateHelper.java, SimpleDate.java, SimpleTime.java. Compile all three files.

Step 1. Create a new program, NewDateTester.java, with a main method that contains the following lines.

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

Compile and execute NewDateTester. Record the results.


 
 
 

Step 2. Add the following lines to the main method of NewDateTester.

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

Recompile and execute NewDateTester. Record the new results.


 
 
 

Step 3. Were the results in steps 1 and 2 the same? If so, why? If not, why not?


 
 
 

Step 4. Add the following lines to the main method of NewDateTester.

    helper.printRange(out, new SimpleTime(1964,6,17,5,0), 
                           new SimpleTime(1964,12,3,11,30));

What do you expect the new output to be?


 
 
 

Step 5. Recompile and execute NewDateTester. Record the new results. Were they what you expected? Why or why not?


 
 
 
 
 
 

Step 6. Add the following lines to the main method of NewDateTester.

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

What do you expect the new output to be?

Step 7. Recompile and execute NewDateTester. Record the new results. Were they what you expected? Why or why not?


 
 
 
 
 
 

Experiment O3.2: NewPoints and PointPrinters

Required files:

Step 1. Make copies of Point.java, PointFun.java, and NewPoint.java. Change the body of the main method of PointFun.java to read

    // We'll be generating some output.
    SimpleOutput out = new SimpleOutput();
    // The point we'll be playing with.
    Point pt = new Point(2,3);
    // Something to help us print
    PointPrinter printer = new PointPrinter();
    // Print some basic information
    printer.print(out,pt);
    // Move it right a little bit.
    out.println("Shifting right by 0.7");
    pt.shiftRight(0.7);
    // Print current information
    printer.print(out,pt);
    // Move it right a little bit.
    out.println("Shifting up by 2.5");
    pt.shiftUp(2.5);
    // Print current information
    printer.print(out,pt);
    // Move it left a little bit.
    out.println("Shifting left by 10.2");
    pt.shiftLeft(10.2);
    // Print current information
    printer.print(out,pt);

Compile all three classes and execute PointFun. Record your results.


 
 
 
 
 
 

Step 2. Update PointFun to use a NewPoint wherever it used a Point. Summarize your changes here.


 
 
 
 
 
 

Step 3. Try to compile PointFun. What happens?


 
 
 
 
 
 

After writing your answer, you may want to look at the notes.

Step 4. Update PointPrinter so that it can print both Points and NewPoints. Recompile PointPrinter. Summarize your changes here.


 
 
 
 
 
 

You may also want to look at a suggested update.

Step 5. Recompile PointFun. Are you successful? If so, why would you have been successful here and not in step 3? If not, why not?


 
 
 
 
 
 

After recording your answer, look at the notes on the subject.

Step 6. Execute the newly compiled PointFun and record your results. Are they what you expected? Why or why not?


 
 
 
 
 
 

Step 7. Record any general observations you have about the process of creating and using variant of the Point class in this experiment.


 
 
 
 
 
 

Experiment O3.3: Using ExtendedPoints

Required files:

Step 1. Make copies of Point.java, PointFun.java, PointPrinter.java, and ExtendedPoint.java. The body of the main method of PointFun should be identical to that of step 1 of Experiment O3.2. Compile all four classes. Execute PointFun. Record your results.


 
 
 
 
 
 

Step 2. Update the main method of PointFun so that it uses ExtendedPoints instead of Points. Do not modify PointPrinter! Summarize your changes.


 
 
 
 
 
 

Step 3. Compile and execute PointFun. Were you successful? If not, summarize and explain the error messages. If you were successful, compare your answer to step 3 of Experiment O3.2. When you are done, read the notes on this step.


 
 
 
 
 
 

Step 4. Record any general observations you have about the process of creating and using a subclass of the Point class in this experiment.


 
 
 
 
 
 

Experiment O3.4: Printing points

Step 1. Make copies of the two files and compile them.

Step 2. Create the following Printable class

public class Printable {
  /**
   * Convert the current object to a string.
   */
  public String makePrintable() {
    ...
  } // toString()
} // class Printable

You will need to fill in something appropriate for the ellipses. Compile this class and correct any errors you encounter. What did you use in place of the ellipses?


 
 
 

Step 3. Update SimpleOutput.java to include a println(Printable) method. Summarize the changes you've made to add such a method.


 
 
 
 
 
 

After doing so, you may want to read the notes on this step.

Step 4. Create a new class, PrintablePoint that extends Point. Add a makePrintable method to PrintablePoint. Compile PrintablePoint and correct any errors. Summarize the structure of PrintablePoint.


 
 
 
 
 
 

Step 5. Create a new class, PointPrinter, that creates and prints a PrintablePoint. The main method of PointPrinter should be similar to

    SimpleOutput out = new SimpleOutput();
    PrintablePoint pt = new PrintablePoint(1,4);
    out.print("The point is ");
    out.println(pt);

What do you expect to happen if you try to compile or execute PointPrinter? Why?


 
 
 
 
 
 

Step 6. Compile and execute PointPrinter. What happened? Is this what you expected? Explain the results.


 
 
 
 
 
 

Step 7. Update PrintablePoint so that it extends both Point and Printable. The class header will read

public class PrintablePoint 
  extends Point
  extends Printable {
  ...
} // class PrintablePoint

What happens when you try to compile this new class? Why?


 
 
 
 
 
 

Step 8. Change Printable from a class to an interface. Summarize the changes you've made.


 
 
 
 
 
 

Step 9. Try to compile PrintablePoint. What happens? How does this result differ from the result in step 7, and why?


 
 
 
 
 
 

Step 10. Update PrintablePoint so that it implements Printable rather than extending it. Try to compile the revised class. How do your results differ from those in steps 7 and 9? Why do they differ?


 
 
 
 
 
 

Step 11. Execute PointPrinter and record the results. Are they what you expected? Why or why not?


 
 
 
 
 
 

Experiment O3.5: Printing revisited

Required files:

Step 1. If you have not already done so, make a copy of the required files and add a println(Printable) method to SimpleOutput.

Step 2. Create a new class, Person, which implements Printable. Person should have one field, a string representing the name of the person. Make sure you include an appropriate constructor for setting that name. Do not include a makePrintable method! Summarize your design for the class.


 
 
 
 
 
 

Step 3. Create a new class, PersonPrinter with a main method that reads

    SimpleOutput out = new SimpleOutput();
    Person author = new Person("Mary P. Boelk");
    out.print("The author is ");
    out.println(author);

Compile and execute PersonPrinter. Record the results. Are they what you expected? Why or why not?


 
 
 
 
 
 

Step 4. Add a makePrintable method to Person that that returns the name of the person. Recompile Person and execute PersonPrinter. Record the results. Were they the same as in the previous step? Why or why not?


 
 
 
 
 
 

Step 5. Change the keyword implements in Person to extends. Attempt to recompile Person. What happens? Why do you think this is? After writing your answer, restore the implements.


 
 
 
 
 
 

Step 6. Build a Student class that extends Person. Students should include majors in addition to names. Do not explicitly indicate that Student implements Printable and do not include a makePrintable. Compile Student and correct any errors you encounter. Summarize the design of your class.


 
 
 
 
 
 

Step 7. Update the main method of PersonPrinter to print a Student rather than a Person. For example, you might write

    Student generic = new Student("Gen R. Ic", "None");
    out.print("This is what we know about the student: ");
    out.println(generic);

Compile and execute the revised PersonPrinter. Were you successful? What might success indicate?


 
 
 
 
 
 

Step 8. Add a makePrintable method to Student. Recompile Student and execute PersonPrinter. Record your results. What do they suggest? Note that you never explicitly indicated that Students are printable.


 
 
 
 
 
 


Post-Laboratory Problems

Problem O3-A: makePrintable vs. toString

Java recommends that you use toString rather than makePrintable when converting objects to strings. In fact, Java will often automatically use toString if available to convert an object to a string. Write an appropriate class or classes to test this assertion. (Note that the instructions for this problem are purposefully left vague so that you will make some effort to understand both the assertion and how you might check it.)

Given this experience, it behooves you to update the classes from the experiments to define and use toString rather than makePrintable.

Problem O3-B: Cloning points

One of the difficulties with using members of subclasses in place of members of superclasses is that the methods that use these objects sometimes make copies. For example, the Plane class includes the line

      Point copy = new Point(pt.getX(), pt.getY());

This means that even if we call plot on a GridPoint or a ComparablePoint, it ends up turning the point into a normal Point. How do we avoid this? By adding a clone method to all of our classes. This method makes a copy of the current object. That is, if Point, GridPoint, and ComparablePoint all provide clone methods, then the methods that use Points can easily make copies using the clone method, rather than explicitly making a new copy.

Update Point, GridPoint, ComparablePoint, and any other descendants of the Point class to include clone methods.

Update TextPlane and Plane to use those methods.

Problem O3-C: Abstract classes

Java provides a third inheritance-like mechanism through abstract classes. Investigate the uses of abstract classes and summarize their similarities to and differences from both normal inheritance and interfaces.

Problem O3-D: Interfaces vs. inheritance

Describe an instance (other than the ones in the laboratories) in which it is more appropriate to use inheritance rather than interfaces.

Describe an instance (other than the ones in the laboratories) in which it is more appropriate to use interfaces rather than inheritance.

In a few sentences, summarize guidelines a programmer might use in deciding whether to use interfaces or inheritance.

Problem O3-E: Defining polymorphism

There are a number of ways in which computer scientists define polymorphism. See how many you can find. Are they all essentially equivalent, or are there substantial differences between the different definitions?


Notes

Experiment O3.2, Step 3. Because PointPrinter's print method was designed to work with Points and not NewPoints, Java won't let you call the print method on a NewPoint.

Experiment O3.2, Step 4. Since PointPrinter needs to support both Points and NewPoints, it makes sense to maintain the existing method and add an additional method that differs only in that we use NewPoint instead of Point. The method might look like

  /**
   * Print a point using a particular output object.
   */
  public void print(SimpleOutput out, NewPoint pt) {
    out.println("(" + pt.getX() + "," + pt.getY() + ")");
    out.println("  distance from origin: " + 
                pt.distanceFromOrigin());
  } // print(SimpleOutput, NewPoint)

Experiment O3.2, Step 5. If you have done everything appropriately, compilation should be successful in this step. Why? Before there was no print method that could accept a NewPoint as a parameter. Now there is.

Experiment O3.3, Step 3. In Experiment O3.2, you found that it is not possible to pass a NewPoint as a parameter to the print method of PointPrinter. In this experiment, you found that it is possible to pass an ExtendedPoint as a parameter to the same function without modifying print! This illustrates the principle that you can use an element of a subclass wherever an element of a superclass is called for.

Experiment O3.4, Step 3. You will need to create the appropriate method using code similar to that which appears in the discussion. In addition, you will need to import the new Printable class.


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

Source text last modified Wed Oct 6 12:41:50 1999.

This page generated on Tue Oct 26 15:39:13 1999 by Siteweaver.

Contact our webmaster at rebelsky@math.grin.edu