10. Using the Class Hierarchy

Part of CS:2820 Object Oriented Software Development Notes, Fall 2020
by Douglas W. Jones
THE UNIVERSITY OF IOWA Department of Computer Science

Polymorphism

Java supports polymorphic classes, that is, classes where there are multiple possible implementations. In fact, polymorphism was added to Simula '67, the original object-oriented programming language, precisely to allow for the kind of variation that we need.

Specifically Java, C++ and their ancestor Simula '67 all allow us to introduce new classes that extend an existing class. For example, in Java we could have:

/** Intersections with a stop light
 *  @see Intersection
 */
class StopLight extends Intersection {
}

How do these new subclasses differ from the parent class? The simplest place they differ is in the toString method, so we can immediately create new methods for that:

/** Intersections with a stop light
 *  @see Intersection
 */
class StopLight extends Intersection {
    String toString() {
        return (
                "intersection " +
                name +
                " stoplight"
        );
    }
}

In the above, we've made the output of the toString() method recreate our input text, unless the input contained tabs or multiple spaces between the words. Information about those details is lost (deliberately) by the scanner we are using.

Note that wherever it is legal to have an Intersection, it is now legal to have a StopLight. Consider the following declarations in a hypothetical bit of Java code:

Intersection i;
StopLight s;

Here assignments i=s is legal because i can hold any kind of intersection. It is also legal to write i=new StopLight(). In the opposite direction, you cannot be so free. s=i is illegal — what you have to write if you want this is s=(StopLight)i which means "check to see that i is actually a StopLight and then, if it is, do the assignment; if it isn't, throw an exception."

Note that we have a challenge here: Should uncontrolled intersections be the default class or should uncontrolled intersections be a subclass of a generic intersection class. In the latter case, no instances of the generic class would ever be created. We can enforce this by declaring class Intersection to be an abstract class.

Constructors for Polymorphic Objects

When you create a new object, you must pick its actual class. Once an object is created, you cannot change its class. So, we must change the code to create intersections. Here is the old code that called the constructor: for readNetwork()

static void readNetwork( Scanner sc ) {
    while (sc.hasNext()) {
        // until the input file is finished
        String command = sc.next();
        if ("intersection".equals( command ))
            new Intersection( sc );
        } else if ("road".equals( command ))
            new Road( sc );
        } else {
            // Bug: Should probably support comments
            Errors.warning(
                "'"
                + command
                + "' not a road or intersection"
            );
        }
    }
}

We need to change the part for creating intersections. Either we must have readNetwork() decide what kind of intersection to construct, or it must call something in class Intersection that makes that decision. What it calls in Intersection cannot be a constructor, because if it is, then that very fact makes it too late to decide what kind of intersection to construct.

This creates a problem. Our current idea for an input language is arranged like this:

intersection X stoplight

In our current code, the Intersection constructor is responsible for scanning the identifier X from the above as well as anything that follows it. Perhaps we should change the language to something like this:

intersection stoplight X

If we do this, buildModel can learn the class it is supposed to create before calling a constructor. We could go farther and completely eliminate the keyword intersection from the input language and just have a large collection of keywords like stoplight and perhaps stopsign and roundabout. We won't do that.

If we use the form intersection stoplight X for complicated intersections and interesection X for default ones, we could call the right constructors in buildModel with code like this:

        String command = sc.next();
        if ("intersection".equals( command ))
            if (sc.hasNext( "stoplight" )) {
		sc.next(); // discard the keyword
                new StopLight( sc );
            if (sc.hasNext( "stopsign" )) {
		sc.next(); // discard the keyword
                new StopSign( sc );
                ...
            } else {
                new NoStop( sc );
            }
        } else if ("road".equals( command ))
            ...

This solution would force readNetwork() to know about every subclass of Intersection. As a general rule, one way to improve the maintainability of large programs is to limit the need for one part of the program to know anything about internal details of another part. From the top level, all we need to know is that there are intersections. Only within class Intersection is there any reason to know that there are subclasses. Therefore, we will abandon this solution.

Subclass Constructors.

We never intend to allow anyone to create an object of class Intersection. There are two ways to do this in Java. One approach is to prevent anyone outside the class from calling its constructor. For example, we could declare the constructor to be private:

        // constructor
        private Intersection() {}

Declaring the constructor to be private prevents any code from outside class Intersection from creating any objects of this class, but it still allows code within the class to do so. Another approach to solving the problem is to declare the class to be abstract like this:

abstract class Intersection() {
	String name;
        ...
}

Declaring the class to be abstract makes it illegal to call new Intersection() anywhere in the program. The only way to create an instance of an abstract class is to create an instance of one of its subclasses. So, if StopLight is a subclass of Intersection we can call new StopLight(), and the new StopLight will contain all the fields of an Intersection.

It is legal for an abstract class to have a constructor, but that constructor can only be used from within the constructor of one of the subclasses. That is, the constructor for StopLight can use the constructor for Intersection to initialize the fields that it inherits from Intersection.

No matter how we prevent naked instances of Intersection from being created, we must add constructors for its subclasses. For example, we can begin with a constructor for NoStop that looks something like this:

    public NoStop( Scanner sc ) {
        // scan and process one intersection
	String name = sc.getNext( "???", "intersection missing name" );
        if (RoadNetwork.findIntersection( name ) != null) {
            Errors.warning( "Intersection " + name + " -- redefined." );
            // Bug:  Can we prevent creation of this object?
        }
    }

The code for StopLight is similar. Before we give this, though, note that we have a bug notice that is repeated three times in our code. In class Road and again, in classes NoStop and StopLight, we have repeated the same basic bug notice asking how we detect improper end of line. What we need is a service method to solve this problem in one place, instead of duplicating code everywhere.

Initializing Subclass instances

Here is the constructor for class StopLight augmented to use the above code:

    public StopLight( Scanner sc ) {
        // scan and process one stop-light intersection
	String name = sc.getNext( "???", "intersection missing name" );
        if (RoadNetwork.findIntersection( name ) != null) {
            Errors.warning( "Intersection " + name + " redefined." );
            // Bug:  We should prevent creation of this object!
        }
	// Bug:  Excessive code duplication if all intersections start as above!

	// Bug:  Missing anything specific to stop lights
    }

This constructor will be the basis of some extended discussion, first because of the bug notices. In the event that an attempt is made to redefine an intersection that already exists, we need to suppress the definition. Second, it is foolish to do computationally complex things like string concatenation in order to construct error messages that will never be printed. This issue shows up all over our code in constructing messages passed to the MyScanner get methods. String concatenation is expensive, and we should not do it until we learn that the string will actually be used! In real use, most of those error messages we computed and passed as parameters are never ever used.

This constructor still doesn't do anything specifically related to stoplights, and we've added some bug notices since we really don't want to have to write duplicate code at the head of every constructor for subclasses of Intersection. The code at the end of each constructor says what kind of intersection it is, and it is just one method call, so that duplication is not flagged as a bug, even though we write out the code as 4 lines, or 5 if you include the bug notice.

Furthermore, it really isn't the constructor we want, because the syntax we want is intersection X stoplight, and we really want to call something like a constructor in class Intersection that will take care of both grabbing the name X and creating the right subclass.

Factories, not Constructors

The program structure we want is something like the following:

In reality, a Java constructor is just a static method that implicitly gets an uninitialized object, initializes it, does anything lese, and returns the new object. Any constructor can be rewritten as a static method that uses the default constructor (the one that does no initializations) to do its job, although this may require that final be removed from some variables declarations. Removing final tags from working code leaves working code, so this doesn't change anything.

We call a method that is used to construct objects a factory method. Sometimes, we will even create factory classes, where each instance of that class can be used to construct objects. In that case, the factory instances are usually initialized with the customizations they will apply to the objects they manufacture.