Consider the problem of maintaining a directory of students, staff, and faculty. For the purposes of this lab, suppose an entry in the directory has the following fields:
All Entries
Students
Staff
Faculty
The directory should contain these methods:
This problem illustrates some common characteristics of a variety of applications. At one level, we want to consider all people in the directory as being similar -- the directory should be a collection of entries. At another level, each type of person (e.g., students, staff, faculty) has special characteristics.
One way to accommodate such multiple levels is develop an appropriate class hierarchy. In this case, we might begin with an Entry class and then define three subclasses, Student, Staff and Faculty. Schematically, this is shown in the following diagram:
Noting that both Staff and Faculty have offices and office extensions, this hierarchy might be refined further, as follows:
For an extended example, this alternative hierarchy might have several advantages as more common characteristics of staff and faculty were identified. For simplicity in what follows, however, we adopt the first of these class hierarchies, focusing on classes Entry, Student, and Faculty for individuals and class SchoolDirectory for the overall structure. The development of the Staff class is left as an exercise.
The Entry class should contain the data and capabilities that we might want for the records of all types of people. In reviewing the problem, some obvious methods involve the creation of an entry, printing a record, and checking if an entry's name matches a given name. In addition, if names are to be stored alphabetically, it will be convenient to be able to compare if one entry equals another or if one entry comes before another alphabetically. Program Entry.java contains a simple version of such an Entry class.
Design Principle: An object or a class is a self-contained entity, and it should include those data elements and operations that naturally support that entity. For the Entry class, we identify both data and operations which seem closely related to each other and to our image of what directory entries might contain.
As you review this Entry class, note that it contains its own testing code in its main method. When this code is run by itself, this main method will run to provide approprate testing. When Entry is imported and another class is run, then that class's main will provide the basis for program execution.
Copy Entry.java to your account, compile and run it, and check that you understand how it works.
With Entry defined, we can define the Student class by extending Entry and adding only the few new required fields. The resulting code is Student.java
For the most part, the code in Student.java is straightforward. However, in two places, we want to utilize existing code in super class Entry as a first step in later processing. Specifically, in initializing a Student, we first call upon Entry's constructor to initial the inherited fields, firstName, lastName, and eAddress. Such a reference to a method in a super class uses the keyword super. Similarly, a reference to the print method in the super class Entry is given by super.print.
Copy Student.java to your account. Again, compile and run the program, and check that you understand how it works.
Copy Faculty.java to your account, run it, and review how it works.
In Faculty.java, the current print method relies on Entry's print method to begin the process. Suppose, however, that we want to change the format of printing, so a faculty member's department follows on the line immediately after the person's name rather than later in the listing. Modify print in Faculty.java to make this change. DO NOT CHANGE Entry in any way!
Before writing the SchoolDirectory class, we must decide how to store entries. As suggested earlier, one approach is to keep the entries, arranged alphabetically by name, in an array -- but what size should the array be? One approach is to create an array of some size maxSize and maintain a separate variable currentSize. We can keep adding entries to the array until currentSize equals maxSize. At this stage, we could generate a larger array (perhaps twice the size), copy the old array to the new, larger one, and then make the insertion. Such an approach does not place an arbitrary limit on the directory size, but also does not waste space excessively.
With this approach to array expansion, we can insert new elements in name order, following the insertion process from an insertion sort. Printing can follow a linear scan of the array, and data retrieval can follow from a binary search. For data retrieval, however, we still must decide what the search should return. A common approach would yield the entry in question, so the program could do subsequent processing if desired. All of this work follows algorithms and approachs seen previously in the course, and we can present a first version of program SchoolDirectory.java.
While the coding within SchoolDirectory.java follows directly, the resulting program illustrates several important points:
Method add depends upon both Student and Faculty being subclasses of Entry. In particular, the parameter person for add must be an Entry. Through inheritance, every Student and Faculty object is just a special case of an Entry object, so this requirement holds. Inheritance sometimes is said to reflect an is-a relationship -- every Student is-a Entry, so we can use a Student object anywhere the code requires an Entry object.
Further, add only relies on methods guaranteed to exist for an Entry, so the logic works correctly for add -- regardless of whether the person is just an Entry or a special Entry (e.g., a Student or Faculty).
Inheritance identifies methods that are available for the class and any subclass, so objects of different subclasses may be used in a broad framework.
Method print may be deceptively simple. As with add, print just cycles through a sequence of entries and prints them. However, recall that print for a Student is not the same as print for a Faculty. By writing entryArray[i].print(), we send the print message to the object entryArray[i]. The object then interprets the message according to what class it is. Students use their print method, while Faculty use theirs. In object-oriented problem solving, this interpretation of the method by an object as the code is running is called polymorphism. With polymorphism, the program can identify the name of a method when the program is written and compiled. However, the details of the method are not decided until the program is actually run, as the specific class for the method may change from one run to the next.
In Java, all classes are considered subclasses of a special class Object. The Object class provides a few basic operations, such as a default constructor. In addition, Object contains a toString method, which returns a string representing some data in an object. While we have not used this method explicitly in the past, it has been implicit in many of our out.print statements. Specifically, consider the statement
out.print("The output is" + data);
As print ultimately requires a string for output, the Java compiler must find a mechanism to find a printable string to represent the data object. In effect, Java accomplishes this task by invoking the toString method inherited from Object. While the details of this method may change from class to class, toString always exists in some form for any class. Polymorphism provides the mechanism for the details of this method to be interpreted when needed during execution.
Add an update capability to this application, as follows:
Add an update method to class Entry and each of its subclasses to provide a framework to change data within an entry (except the name). update should use no parameters. Rather, the method should prompt the user at the keyboard for new values of all fields within the Entry, except for the person's name which should remain unchaned.
Add an update method to class SchoolDirectory. This update method should use two String parameters, for the person's first and last name. update then should use the lookup to locate the correct Entry within the directory, and the Entry's update method to effect the change. If the specified name is not in the directory, a warning message should be printed.
Add a remove method to class SchoolDirectory to delete a person with a given first and last name from the directory.
Extra Credit: Write a Staff class which extends Entry, according to the field specification given at the start of this lab.
While class SchoolDirectory worked reasonably well, the lookup method contains some awkwardness. In particular, the method is supposed to return a specified Entry. If the person is found, the method works as desired. The difficulty arises if the person is not present.
In SchoolDirectory.java, lookup resorts to returning a null value if no such person is found. While this approach satisfies the specification of the method by yielding a special value, this approach can cause additional effort in an application, as shown in the corresponding main method. Specifically, lookup always returns something, and that object must be tested each time to determine if the returned value is null.
A cleaner approach is to break the normal flow of processing, using an exception. In general, an exception is any event that may require a change in the normal flow of processing. In Java, a program "throws" an exception when the unusual event is detected. Statements in the block calling the method then "catch" the exception, allowing appropriate action to be taken. To illustrate this approach for the SchoolDirectory example, program SchoolDirectoryAlt.java takes advantage of a NoSuchElementException, already built into Java in the java.util class.
In this revised program, the return null statement at the end of the lookup method is replaced by
// if person not found, generate an exception throw new NoSuchElementException();
With this addition, normal processing is interrupted if the desired name is not found. Throwing the exception allows the end of the main method to be much cleaner. The main structure for this code is
try { . . . dir.lookup("- - - -", "- - - -"); . . . } catch (NoSuchElementException e) { out.println("Directory Entry not found: "); out.println(" Exception caught: " + e); }
In this try ... catch block, the try keyword indicates that processing should shift to the catch section if any exceptions are encountered in the block. The block itself then can be considerably simpler.
More generally, a program can contain several try ... catch blocks (even one for each lookup), and exceptions can be handled differently in each one if desired.
Modify the update and remove methods in SchoolDirectory to respond appropriately if lookup throws a NoSuchElementException exception. Note that your changes should effect the methods (and testing) in SchoolDirectory only. No changes should be made in Entry or any of its subclasses.
This document is available on the World Wide Web as
http://www.walker.cs.grinnell.edu/courses/153.sp00/lab-generalization.html
created May 2, 2000 by Henry M. Walker
last revised May 3, 2000
Henry Walker (walker@cs.grinnell.edu)