next up previous
Next: 1.5.4 Conditional Statements Up: 1.5 The Union and Previous: 1.5.2.0.1 When Unions are

  
1.5.3 Defining Instance Methods for a Composite Class

In Section 1.4.1, we showed how to define simple (instance) methods for the individual class Entry. But we did not show how to express operations that process all of the different forms of data defined by a composite hierarchy. Since each different form of data in a composite hierarchy is represented by a distinct concrete class, we can write a separate method definition for each kind of data.

Consider the following example. Assume that we want to define a method

String firstAddress(String name)
on DeptDirectory that returns the address for the first person in the directory if the directory is non-empty and the null reference null if it is empty. We can write separate definitions for the method firstAddress in the concrete classes Empty and Cons as follows:
class Empty {
  ...
  String firstAddress() { return null; }
}

class Cons {
  ...
  String firstAddress() { return this.first.getAddress(); }
}

Now assume that x is a variable of type DeptDirectory. If we try to invoke the method firstAddress on x, Java will reject the code as erroneous because the class DeptDirectory does not contain a method named firstAddress. How can we enlarge the definition of firstAddress so that it applies to the class DeptDirectory?

The answer is that we define the method firstAddress in the class DeptDirectory as an abstract method:

class DeptDirectory {
  ...
  abstract String firstAddress();
}

An abstract method is a method without a body. Abstract methods can only appear in abstract classes. Every concrete class extending a class containing abstract methods must provide concrete definitions for the abstract methods that override the missing bodies. This rule guarantees that abstract methods are never attached to objects.

Let us illustrate the process of defining a method over a composite class hierarchy in more detail by defining a method

String findAddress(String name)
on DeptDirectory that finds the address for the person identified by name, assuming that name is in the directory.

First, we must insert the following member somewhere in the class DeptDirectory

abstract String findAddress(String name);
The abstract modifier in the definition indicates that the definition only describes the input and output types for the method, not how it is implemented. Each concrete class extending DeptDirectory must provide a definition for findAddress that includes a code body describing how it is implemented.

The ordering of members within a class typically does not affect program behavior. Nevertheless, it is good programming practice to list class members in a consistent order. We recommend placing dynamic members before static members. Within each category, we recommend the following order: fields, constructors, methods. According to this convention, the abstract method findAddress should be the last member in the DeptDirectory class.

Second, we must provide a concrete definition of the findAddress method in each concrete subclass of DeptDirectory, namely Empty and Cons. Note that this method definition format guarantees that a program includes code specifically to process each data variant. Moreover, in any variant containing a field f of parent type, the method typically invokes itself recursively on f. This approach to defining methods is the direct analog of natural recursion template for defining functions in Scheme. It is so common and so important that object-oriented programming methodologies have labeled it as a separate pattern, called the interpreter pattern, enriching the composite pattern.

Let us return to defining the findAddress method. By definition, there is no Entry in an Empty directory matching the name passed as an argument to findAddress. Hence, findAddress must return a value signaling failure. In Java, the most convenient choice for such a value is null, the reference to no object. In Java, all object values are actually references, so the same object can simultaneously appear as the value of many different variables. Scheme follows exactly the same convention regarding structures. Java also provides the special value null, which is the reference to no object. Java null should only be used to represent a special failure value. It should never be used to represent one of the alternatives in a data definition, e.g., the empty DeptDirectory. The reason for this prohibition is simple: null is not an object. Any attempt to invoke a method on null will generate a run-time error aborting program execution.

The following code is an appropriate definition of the findAddress method in the Empty class.

String findAddress(String name) {
  return null;
}

The definition of findAddress for Cons objects is the only interesting chunk of code in this entire example. Since a Cons object always contains a first Entry and a rest DeptDirectory, the findAddress method must check to see if the passed name matches the name field of first and, depending on the outcome, either return the value of the address or recur on rest.

The object-oriented method has exactly the same recursive structure as the corresponding function definition.

The method can simply be coded as follows:

String findAddress(String name) {
  if (this.name.equals(this.first.getName())) 
    return this.first.getAddress();
  else return this.rest.findAddress(name);
}

Every class contains the instance method equals which takes a single argument of type Object. The Object class at the top of the class hierarchy defines this method. For a String object name, the equals method returns true if and only if the argument object contains exactly the same sequence of characters as name.

In the code above, the expression

this.name.equals(this.first.getName())
invokes the equals method of object in field name on the argument
this.first.getName().
The expression
this.first.getName()
invokes the getName method of the Entry object in the field first to get its name field. Similarly, the expression
this.first.getAddress()
invokes thSe getAddress method of the Entry object in field first to get its address field; the expression
this.rest.findAddress(name)
invokes the findAddress method of the DeptDirectory object in the rest field on the object name.

Notice that a Java instance method with n arguments corresponds to a Scheme function of n+1 arguments because the object containing the method is the implicit argument this. Since the members of this are so frequently accessed in methods attached to the object this, Java allows the field prefix

this.
to be omitted! Hence, the definition of findAddress could have been written
String findAddress(String name) {
  if (name.equals(first.getName())) 
    return first.getAddress();
  else 
    return rest.findAddress(name);
}
Since explicit references to this clutter code making it more difficult to read, we will generally omit them. (In some situations, a method must refer to the entire receiver object this rather than one of its fields. In such a situation, the use of the keyword this is essential.)


Finger Exercise Enter the text for the DeptDirectory example in the DrJava Definitions window. Edit the main method to construct some sample directories. Compile the program and try some test lookups in the Interactions window. Write a method findPhone analogous to findOffice and test it on similar examples. Remember to use the same design steps that you learned for Scheme.


next up previous
Next: 1.5.4 Conditional Statements Up: 1.5 The Union and Previous: 1.5.2.0.1 When Unions are
Corky Cartwright
2000-01-07