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:
DateHelper.java
NewPoint.java
Point.java
PointFun.java
PointPrinter.java
Printable.java
SimpleDate.java
SimpleInput.java
SimpleOutput.java
SimpleTime.java
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.
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 SimpleTime
s, as in
helper.printRange(out, new SimpleTime(1964,6,17), new SimpleTime(1964,12,3));
Even though printRange
expects two SimpleDate
s,
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.
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
Point
s 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.
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
NewPoint
s without first converting them to
Point
s.
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.
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
ExtendedPoint
s. Similarly, because PointPrinter
provides a print(Point)
method, you can print
ExtendedPoint
s.
You will investigate this type of reuse in Experiment O3.3.
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
interface
is used instead
of the keyword class
, and
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.
Name: ________________
ID:_______________
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?
NewPoint
s and PointPrinter
sRequired 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 Point
s and
NewPoint
s. 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.
ExtendedPoint
s Required files:
Point.java
PointFun.java
PointPrinter.java
ExtendedPoint.java
(created and modified in previous laboratory
sessions)
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
ExtendedPoint
s instead of Point
s.
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.
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?
Required files:
Printable.java
(the interface, not the class)
SimpleOutput.java
(as modified in the previous experiment)
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
Student
s are printable.
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
.
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 Point
s 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.
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.
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.
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?
Experiment O3.2, Step 3.
Because PointPrinter
's print
method
was designed to work with Point
s
and not NewPoint
s, Java won't let you call the
print
method on a NewPoint
.
Experiment O3.2, Step 4.
Since PointPrinter
needs to support both Point
s
and NewPoint
s, 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.
[Front Door] [Introduction] [Code]
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