11. Polymorphism

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

 

Creating Subclasses

In our road-network example, there are many kinds of intersections. There are intersections with stop signs on every entry, there are intersections with traffic lights, and there are uncontrolled intersections. We could deal with these by adding an intersection type field to class Intersection; that is how programmers in languages like C and Pascal routinely handled such differences. Object-oriented languages like Java allow us to follow an alternative path. What we will do here is create a subclass of Intersection for each kind of intersection:

class FourWay extends Intersection {
        /** Intersections with stop-signs on every incoming road.
         *  @see Intersection
         */

}

class NoStops extends Intersection {
        /** Intersections with neither stop-signs nor stop lights.
         *  @see Intersection
         */
}

Initially, these classes have no attributes at all, we will extend them after we look at where we will be using them:

Extending The Highway Description

Our initial highway description language looked something like this:

intersection a
intersection b
road a b 45.0
road b a 72.0

We need to add a type field to intersection descriptions, giving us something like this:

intersection 4way a
intersection nostop b
road a b 45.0
road b a 72.0

Parsing The Highway Description

The parser for the highway description file, in RoadNetwork.readNetwork() needs something like the following added to it:

        private static void readNetwork( Scanner sc ) {
                /** Read a road network, scanning its description from sc.
                 */
                while (sc.hasNext()) {
                        // until the input file is finished
                        String command = sc.next();
                        if ("intersection".equals( command )
                        ||  "i".equals( command )           ) {
                                String kind = sc.next();
                                if ("nostop".equals(kind)) {
                                        inters.add( new NoStops( sc ) );
                                } else if ("4way".equals(kind)) {
                                        inters.add( new FourWay( sc ) );
                                } else {
                                        Errors.fatal(
                                                "intersection '"
                                                + kind
                                                + "' -- unknown type"
                                        );
                                }
                                // inters.add( new Intersection( sc, inters ) );
                        } else if ("road".equals( command ) ...

Here, the old code has been commented out, and the new code is shown in bold face.

Note that Java allows an alternative way to express this that is very tempting using a case-select construct. We might have tried to write the new code as follows:

                                switch (sc.next()) {
                                case "nostop":
                                        inters.add( new NoStops( sc ) );
                                        break;
                                case "4way":
                                        inters.add( new FourWay( sc ) );
                                        break;
                                default:
                                        Errors.fatal(
                                                "intersection '"
                                                + kind
                                                + "' -- unknown type"
                                        );
                                }

There are several things to note about this: First, while this code is a bit less verbose than the original, it is no more compact. The break statements add lines of code.

Second, there is a problem in the default case because we want to use kind in our error message. This used to be the value returned by sc.next. This forces us to replace the first line shown above with thse two lines:

                                String kind = sc.next();
                                switch (kind) {

Third, note that, for integer and enumerated types, there are very efficient ways to implement the case-select operation that are much faster than a cascade of if statements. This is not true for strings. What Java actually does when it sees a case-select control structure made using strings is generate a cascade of if statements looking like this:

                                String kind = sc.next();
                                if (kind.equals( "nostop" )) {
                                        inters.add( new NoStops( sc ) );
                                } else if (kind.equals( "4way" )) {
                                        inters.add( new FourWay( sc ) );
                                ...

Finally, this code is unsafe, something that is not obvious from the original case-select code but should be obvious in the equivalent cascade of if statements. If sc.next() returns null, this code will fail with a null-pointer error. We could rewrite it as follows, of course, but this only makes the code even longer:

                                String kind = sc.next();
                                if (kind == null) {
                                        Errors.fatal(
                                                "Intersection without type"
                                        )
                                } switch (kind) {

Had we simply stayed with the original cascade of if statements, the code would have run correctly even if sc.next() returned null because the + operator, when used for string concatenation, creates an eccentric but not inappropriate output, treating null pointers as the string null. The result is an error message that makes some sense.

So, we will not opt to use the case-select mechanism here.

Initializers for Subclasses

The code in our input parser given in the above sectin calls the initializers for new NoStops and new FourWay, so we'd better write these. Here is example code for one of them:


class FourWay extends Intersection {
        /** Intersections with stop-signs on every incoming road.
         *  @see Intersection
         */

        // initializer
        public FourWay( Scanner sc ) {
                /** Initialize a four-way stop-sign interseciton.
                 */
                super( sc );
        }

At this point, there is nothing to distinguish one kind of intersection from the other, so the job is "kicked down the road" to the initializer for the parent class. In the call super( sc ); here, the keyword super refers to the class extended by this class. It is a way of walking up the tree of subclasses. We leave it to the parent class to process the intersection name and check that the remainder of the line is blank.

Overriding Methods of the Parent Class

The RoadNetwork.writeNetwork() method calls i.toString() for each intersection in the network, and we need to override the default toString() method provided by class Intersection, since this method outputs "intersection name" for each intersection, and we need it to output the type of the intersection. We do this by adding a new toString() method in each subclass of Intersection. Consider this example:

class NoStops extends Intersection {
        /** Intersections with neither stop-signs nor stop lights.
         *  @see Intersection

        ...

        public String toString() {
                /** Convert an intersection back to its textual description
                 */
                return "intersection nostop " + name;
        }
}

In general, when one class extends another, it can override any of the methods of the parent class.

In this case, however, we do not want to merely provide an alternative method. We want to prevent anyone from calling the toString() method of class Intersection. Java allows us to do this by declaring the method in the parent class to be abstract.

The choice of the keyword abstract is a bit of a puzzle. What it really means is that thd declaration establishes a template that must be implemented by every subclass of the class where the abstract method is declared. Here is the definition that replaces the original toString() method in class Intersection:

abstract class Intersection {
        /** Intersections join roads
         *  @see Road
         */

        ...


        public abstract String toString();
                /** Convert an intersection back to its textual description
                 */
}

Note that when a class contains any abstract methods, that class must be declared as an abstract class. Abstract classes cannot be instantiated. Thus, it is now illegal to write new Intersection(), but it is still legal to declare variables and formal parameters such as Intersection i. The following two assignments to the variable i would be legal if it was declared as in the previous sentence:

        i = new FourWay( sc );
        i = new NoStops( sc );

For comparison, in C, declaring a function as, for example, int f(); declares it to be an integer function that will be defined at some unspecified later point in the code. C does not require a keyword such as abstract. Merely using a semicolon where a curly brace was expected is sufficient, and C calls such a declaration a function prototype, where it calls the complete declaration of the function, with its body a function definition. If the designers of Java wanted to use C terminology, they would have referred to abstract methods as method prototypes, and they might have referred to abstract classes as class prototypes.

There is no consensus about terminology in programming languages. The designers of Pascal also provided a similar mechanism. In Pascal, if a procedure is declared as procedure p; forward, with the keyword forware in the place where the code for the subroutine would normally be put, this is called a forward subroutine declaration and it means that, later in the code, there must be a complete declaration of the subroutine with a body. Had the designers of Java used Pascal terminology, they could have called abstract methods forward methods. The concept is the same -- here, we describe enough to let you know how to call the code, while later, somewhere else, we will actually provide the code.

Where to next?

We are now ready to start worrying about the semantics of the road network, (and the logic circuit). We need to worry about how to queue cars when they are waiting at a stop sign or red light. (In a logic circuit, we need a way for each gate to keep track of its inputs so that it can produce the right output.)