All of our previous Java programs have taken advantage of previously-existing classes to handle various tasks. This lab carries this idea one step further: modifying or expanding existing classes to meet new needs. In writing these new classes, we will not have to write any code already done in the existing classes; we will just note the desired modifications or extensions. In the jargon of object-oriented problem-solving, we say that our new classes inherit properties (data and methods) from the previous ones.
To motivate our work, we revisit the directory problem from the end of the lab on searching. In that lab, we considered the basic problem of storing name and telephone information and retrieving the numbers by name.
Note: Structures with these basic storage and retrieval operations arise in many contexts. For historical reasons, the indexing value often is called a key or symbol, the associated material is called a value, and the corresponding storage structure is called a symbol table.
When the key is a non-negative integer, such storage can be achieved through an array, where value[i] gives the information associated to the key i. When the key is a string, the lab on searching showed how to use parallel arrays.
In this lab, we follow yet another approach.
A hash table is a specific type of structure which supports two primary methods:
Technical Diversion: Conceptually, for a directory with names and telephone numbers, a hash table might store information in a large array-like structure, such as the following:
As suggested by the figure, the idea of a hash table is to spread the relevant directory entries throughout the array. The particular placement of an item is determined by some function, called a hash function. In practice, many such functions have been investigated.
(Aside: In the diagram, the function used computes the distance between the first letter of the first name and the first letter of the last name. To fit into the 13 spaces in the array, the distance then is taken modulo 13. For "Arnold Adelberg", the first and last names begin with the same letter, the distance between these letters is 0, and the entry for "Arnold Adelberg" is found by looking at position 0 in the array. For "Henry Walker", the letter H is 15 letters away from W. Taking this distance modulo 13 gives the remainder 2, so "Henry Walker" appears in the table by searching from position 2.)
Given a hash function, storage and retrieval from a hash table has two main steps. The hash function indicates where to start in the table. Searching begins from the specified place and continues until the item is found or the end of relevant data is reached. If the hash function spreads data out over a large array, then one can show that typical storage and retrieval operations are extremely fast and efficient.
Specific details of hash tables require considerable analysis of potential hashing functions, use of arrays, and maintenance of structures based on those arrays. With time in this course limited, such work is beyond the scope of this course.
Hash tables in Java: When the keys of a symbol table are objects (for any class with a hashing function hashCode and an equals method), Java contains a predefined class Hashtable, found in class java.util. With this class already existing, we can take advantage of hash tables with little writing of code.
Java's Hashtable has several helpful methods. Here are a few basic ones (beyond creating a new one):
Directory Example:
To illustrate how Java's Hashtable class can be used, consider the Scheme-based lab on Abstract Data Types. In that lab, we created a directory of names and telephone numbers. In particular, that lab created a directory class and utilized methods show, lookup, and add.
Program DirectoryMain1.java achieves the similar operations using some of Java's Hashtable class. In this program, add operation translates directly to put, and lookup translates to get. lookup is related to keys, but is somewhat more complicated.
Review program DirectoryMain1.java, specifically focusing on the put and get operations. Also, copy the program to your account, compile it, and run it. Be sure you understand how Hashtable can be used for storage and retrieval of data.
Program DirectoryMain1.java also illustrates the concept of enumerations -- an idea common to many object-oriented programming languages. Conceptually, an enumeration is simply a sequence of information. Pragmatically, an enumeration is a class which allows one to cycle through a collection of objects. Program Directory1.java shows the main elements in Java -- specifically for class java.util.Enumeration. The relevant code is
for (Enumeration e = table.elements(); e.hasMoreElements() ;) { out.println (" " + e.nextElement());
As noted earlier, table.elements() specifies a method that generates a sequence of elements -- specifically giving an object of Java's class Enumeration. Thus, the code
Enumeration e = table.keys();
creates an Enumeration variable e, and initializes it with the sequence of keys from our table. While enumerations are limited, they have two basic methods:
Program DirectoryMain1.java illustrates the most common use of enumerations -- using an enumeration in a loop to cycle through all elements in a collection.
The enumeration of key values typically depends upon the internal ordering of data within the hash table, which in turn depends upon an underlying hash function. Review the listing of names printed. Is there an obvious pattern or ordering? Explain briefly.
Since the enumerations of keys and values are separate, there is no guarantee that the order given by one of these corresponds to the other. Do the enumerations give the same order here? If so, consider this a matter of luck. If not, note it is unsafe to count on such common orderings.
Now that we have seen how Java's Hashtable might be helpful for a directory, we use it to build a simple directory class SimpleDirectory. A shell for such a class is found at SimpleDirectory.java . Program DirectoryMain2.java uses this SimpleDirectory, following the same test cases seen previously for Hashtable.
Copy SimpleDirectory.java and DirectoryMain2.java to your account, and compile and run them. Review the code to be sure you understand how both pieces of code work.
Within SimpleDirectory, the variable myOut is declared as static, so that only one output object will be created -- regardless of how many SimpleDirectory objects are created.
Modify DirectoryMain2.java, so that two directories are created (dir and dir1). Add lines to insert names into dir1, to retrieve some numbers, and to print all names.
What do you think will happen if SimpleDirectory is changed, so that table is static? Run the revised program, and examine the results. Did the program print what you expected? Why or why not?
In class SimpleDirectory, the local variables table and out are listed as protected. The intention of this keyword is to limit the accessibility of table and out, so an application cannot tinker with these variables directly. Thus, conceptually, protected might be considered in a similar category as private. The details of protected access, however, are somewhat complex and thus are deferred to another lab.
While the SimpleDirectory class has some helpful capabilities, we might want to extend it by adding several methods:
Of course, one approach would be to redefine SimpleDirectory from scratch. However, most object-oriented languages, such as Java, provide a simpler way -- we simply extend the original class SimpleDirectory to get a new class BetterDirectory. This class is found in BetterDirectory, with corresponding test program DirectoryMain3.java.
As this example illustrates, we can extend a class in Java by defining a new class, based on the old, using an extends clause in the declaration of the new class. The new class then has access to all public and protected data of the old class. The body of the new class then contains only the different features.
Jargon: When extending a class, the new class is called a subclass or derived class, and the old class is call a super class. Thus, in the example, BetterDirectory is a subclass of SimpleDirectory, and SimpleDirectory is a super class. We also say BetterDirectory inherits the variables and methods of its super class.
Copy, compile, and run BetterDirectory and DirectoryMain3.java. Review the code to check what happens.
Modify BetterDirectory.java further to define method isIn which checks whether a given name is in the directory.
In defining a derived class, not only can we define new methods, but we can redefine old ones. Write a revised printNames method in BetterDirectory.java, which prints three names to a line. (You will need to add a counter which prints a new line whenever the counter reaches 3 or a multiple.)
The SimpleDirectory and BetterDirectory classes contained a Hashtable as an internal variable. Specific public methods then were defined to provide desired operations: a constructor, add, lookup, printNames, remove, size, and PrintNumbers. Another approach derives a class AltDirectory directly from Hashtable. Since remove and size are already defined in Hashtable, these need not be redefined in AltDirectory.
Write your own AltDirectory class, extending Hashtable and containing a constructor and methods add, lookup, printNames, and PrintNumbers.
Test your AltDirectory class by modifying DirectoryMain3.java to reference AltDirectory rather than BetterDirectory. (This reference occurs three times -- you should make no other changes to DirectoryMain3.java.) Call your revised testing program DirectoryMain4.java.
Since AltDirectory is a subclass of Hashtable, all operations of Hashtable are available in AltDirectory. In contrast, BetterDirectory contains a Hashtable variable. While this variable can utilize methods of Hashtable, such methods cannot be applied directory to BetterDirectory.
The Hashtable class contains an isEmpty() method, which returns true or false according to whether any name-number pairs are stored in the table. Thus, isEmpty() should be already defined in the subclass AltDirectory but not in BetterDirectory. Confirm that this is the case by adding the following lines to both DirectoryMain3.java and DirectoryMain4.java.
// lines to test the isEmpty method for directory dir out.println ("Check if the directory is empty: " + dir.isEmpty());
Compile and run DirectoryMain3.java and DirectoryMain4.java, and be sure you can explain the results.
Inheritance from a super class provides a collection of methods to a derived class. Suppose some of these methods are not desired in the subclass. Since the methods are already defined in the super class, methods by those names must be present in the subclass. One (inelegant) approach would be redefine the method in the subclass to do nothing. Alternatively, one might try to make the method in the derived class private, so it could not be used by applications.
Try each of these approaches to "hide" a previously defined method.
Write method isEmpty() in AltDirectory, so that isEmpty always returns true. Recompile and rerun the program. Describe briefly what happens.
Write method isEmpty() in AltDirectory as a private method which always returns true. Again, recompile and rerun, and describe the result.
This document is available on the World Wide Web as
http://www.math.grin.edu/~walker/courses/153.sp00/lab-inheritance.html