11. Abstract Classes and Methods

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

 

Abstract Classes

Our Intersection class now begins something like this:

/** Intersections pass Vehicles between Roads
 *  @see Road
 */
class Intersection {
    // constructors may throw this when an error prevents construction
    public static class ConstructorFailure extends Exception {}

    public String name;         // textual name of intersection, never null!
    LinkedList  outgoing; // set of all roads out of this intersection
    LinkedList  incoming; // set of all roads in to this intersection

There's a problem here! This class declaration permits us to say new Intersection(), at which point, Java will happily create a new object of class Intersection, despite the fact that no such objects should ever be created.

Java provides a special keyword for modifying class declarations that makes it illegal to construct an instance of that class, the keyword abstract. If we declare the class to be an abstract class, it is still legal to extend that class, creating subclasses, but you cannot create an instance of the class.

When we wrote Errors and ScanSupport, we said that no instances of these classes would ever be created. We can enforce this by declaring them to be abstract as well.

Final Variables

Our Road class now begins something like this:

/** Roads are joined by Intersections and driven over by Vehicles
 *  @see Intersection
 */
class Road {
    // constructors may throw this when an error prevents construction
    public static class ConstructorFailure extends Exception {}

    float travelTime;           // measured in seconds, always positive
    Intersection destination;   // where this road goes, never null
    Intersection source;        // where this road comes from, never null
    // name of a road is source-destination

There's a problem here. The fields travelTime, source, and destination, are all variables, yet we never want these fields to change once their values are set.

Java provides a special keyword for modifying variable declarations that makes it illegal to assign a new value to those variables once they are initialized, the keyword final

For some variables, declaring them as final really does declare them to be constants. This applies to final int and final String for different reasons. In the case of final int, variables of type int are not objects. The variable holds the actual value, and forbidding any assignment to that variable after the first makes the value constant.

In the case of final String, objects of class String are real objects, but class String is immutable; that is, there are no methods in class String that allow you to make any changes to the value of an object of that class. Thus, once a string is constructed, it will retain a constant value. In contrast, many other classes are mutable, which means that there are many methods for changing the value of an object in that class.

A final List, for example, is mutable. Once you initialize it, it is illegal to change what list the variable refers to, but the contents of that list can be changed arbitrarily by adding and deleting elements arbitrarily.

In class Road, it is easy to change source and destination to final declarations. The problem comes when we try to make travelTime final.

class Road {
    // constructors may throw this when an error prevents construction
    public static class ConstructorFailure extends Exception {}

    final float travelTime;           // measured in seconds, always positive
    final Intersection destination;   // where this road goes, never null
    final Intersection source;        // where this road comes from, never null
    // name of a road is source-destination

The problem is apparent when we compare how the constructor initializes these fields.

        source = RoadNetwork.findIntersection( sourceName );
        destination = RoadNetwork.findIntersection( dstName );

        ... code to throw an exception if the above are null ...

        if (sc.hasNextFloat()) {
            travelTime = sc.nextFloat();
            if (travelTime < 0.0F) {
                Errors.warn( "Negative travel time:" + this.toString() );
                travelTime = 99999.0F; // no failure needed, use bogus value
            }
        } else {
            Errors.warn( "Floating point travel time expected: Road "
                        + sourceName + " " + dstName
            );
            travelTime = 99999.0F; // no failure needed, use bogus value
        }

Here, as a human reader, we can easily verify that there is exactly one assignment to travelTime along one path through the if statement. On the other path, however, we override an improper value with a new value. In general, the Java compiler is fairly good at recognizing that a final variable is only assigned once, but it cannot follow complex logic very well. Sometimes, you can easily prove that there will be only one assignment, but the compiler will not be able to figure this out. In general, the solution is to introduce a temporary variable used to compute the final value, and then assign this temporary once to the final variable, when the value is completely determined.

        Float travel; // preliminary value of travelTime
        if (sc.hasNextFloat()) {
            travel = sc.nextFloat();
            if (travel < 0.0F) {
                Errors.warn( "Negative travel time:" + this.toString() );
                travel = 99999.0F; // no failure needed, use bogus value
            }
        } else {
            Errors.warn( "Floating point travel time expected: Road "
                        + sourceName + " " + dstName
            );
            travel = 99999.0F; // no failure needed, use bogus value
        }
        travelTime = travel; // set final value!

Final Variables and Class Hierarchy

Now, let's return to class Intersection. It would be nice to declare the name of an intersection to be final, like this:

abstract class Intersection {
    // constructors may throw this when an error prevents construction
    public static class ConstructorFailure extends Exception {}

    public final String name;   // textual name of intersection, never null!

If we do this, things break. The problem is, the name is set in the constructors for the subclasses. Java will not permit this. Final variables must be set in the constructor for the class itself, not in any subclass. But how can we do this in an abstract class where nobody ever constructs an instance of the class?

The answer is a consequence of the way subclasses are implemented. If class A has class B as a subclass, the actual implementation of an object of class B has all the fields of A followed by the fields of B. For example, consider this code:

class A {
    int A1;
    int A2 = 4;
}
class B extends A {
    int B1;
    int B2;
}

The objects actually created in memory for class B look like this:

class B {
    int A1;
    int A2 = 4;
    int B1;
    int B2;
}

If you pass an object of class B to a method that expects an object of class A, that method will work just fine. It expect an object with 2 fields, and never looks beyond those 2. The fact that the object you passed actually had 4 fields is irrelevant. The first 2 fields obey all the rules for objects of class A.

Now, when you call a constructor with, for example, new B(), what actually happens is, first, the compiler allocates a new uninitialized block of memory big enough to hold an object of class B. Then it executes all the in-line initializations from class A, and then all the in-line initializations from class B, and only then does it exeute the code for the explicit constructor that you called. Within a constructor for class B you have the option of calling any constructor of class A first, so long as you do this immediately at the start of the class B constructor.

This applies even to abstract classes! Constructors of an abstract class may not be used in the outside world, but they may be used in subclasses. As such, constructors in an abstract class should be declared to be protected. Note that constructors may also be declared to be private, in which case they may only be used within methods of the class.

This means we can write a protected constructor for class Intersection that sets the values of the final fields of the new object, using this constructor at the start of every subclass constructor. In fact, we must do so if the abstract class contains any fields that are not initialized by assignments within their declarations. This gives us the following outline for class Intersection:

abstract class Intersection {
    // constructors may throw this when an error prevents construction
    public static class ConstructorFailure extends Exception {}

    public final String name;   // textual name of intersection, never null!

    ...

    protected Intersection( String name ) {
        this.name = name;
    }
}

In each subclass of Intersection, the constructor must begin with a call to the parent class's constructor. For example:

class Stoplight extends Intersection {
    // fields unique to a Stoplight
    ...

    Stoplight( String name ) {
        super( name ); // call the superclass (Interesection) constructor
        ...
        // finish constructing a Stoplight
    }

Methods of an Abstract Class

It is legal to declare methods in an abstract class. We can do this two ways: First, we can declare abstract methods. These are commitments made in the abstract class forcing each concrete subclass to provide an implementation. When you declare an abstract method, you give just the heading, which includes the parameter list but not the method body. For example, every subclass of Stoplight must implement toString(), so we'd like to write this:

abstract class Intersection {
    ...

    public abstract String toString();
}

This would force each subclass of interesection to define toString(). For example,

class Stoplight extends Intersection {
    ...
    public String toString() {
        return "Intersection " + name + " stoplight";
    }

All of the different subclasses of Intersection would use very similar return statements in their toString methods, so it would be nice to provide a concrete toString method in the superclass so that we could write something like this:

class Stoplight extends Intersection {
    ...
    public String toString() {
        return super.toString() + " stoplight";
    }

The problem is, we've already committed to an abstract method in the superclass. Furthermore, the signature of toString is strongly constrained, since we inherited it from class Object, the superclass of all classes. The definition of Object.toString() constrains the signature of all redefinitions of toString(). Because Object.toString() is public, we may not redefine it as private or protected. We must declare it to be public. If we want anything different, we'll have to rename it.

Compacting the code

If we look at the code in the code distributed with Lecture 10, there is quite a bit of duplication. Look at this code from class Intersection:


        String name = ScanSupport.nextName( sc );
        if ("".equals( name )) {
            Errors.warn( "Intersection has no name" );
            sc.nextLine();
            throw new ConstructorFailure();
        }
        ...
        String intersectionType = ScanSupport.nextName( sc );

        if ... {
        } if ("".equals( intersectionType )) {
            Errors.warn( "Intersection has no type: " + name );
            sc.nextLine();
            throw new ConstructorFailure();
        } else { ...

Why not compact this by moving the job of checking to see that there is a name on the input line to ScanSupport:

    /** Get next name without skipping to next line (unlike sc.Next())
     *  @param sc the scanner from which end of line is scanned
     *  @param msg error message to output if there was no string
     *  @return the name, if there was one, or an empty string
     */
    public static String nextName( Scanner sc, String msg ) {
        sc.skip( whitespace );
        sc.skip( name );
        String val = sc.match().group();
        if ("".equals( val )) {
            Errors.warn( "Name expected: " + msg );
            sc.nextLine();
        }
        return val;
    }

This would allow us to compact the code in classes Road and Intersection, for example, rewriting the example above as:

        String name = ScanSupport.nextName( sc, "Intersection ???" );
        if ("".equals( name )) throw new ConstructorFailure();
        ...
        String intersectionType = ScanSupport.nextName(
            sc, "Intersection " + name + " ???"
        );
        if "".equals( intersectionType )) throw new ConstructorFailure();
        if ... {
        } else { ...

We could make a ScanSupport.nextFloat(sc,msg) that worked similarly, and note that ScanSupport.lineEnd(sc,msg) already works this way.

Wasted computation

The normal case is that the methods in class ScanSupport() return without reporting errors, since the normal situation with input data files is that they contain few if any errors.

The problem with our proposed code is that, each time one of these methods is called, there is a very expensive computation for the error message. The simple looking Java expression "a"+"b"+"c" is actually short hand for something like the following:

new StringBuilder( "a" ).append( "b" ).append( "c" ).toString()

Remember, Java strings are not mutable, and Java string concatenation returns a new string object. Java objects of class StringBuilder are identical to strings, except that they are mutable and have a variety of append() and insert() methods for modifying the string. Things are worse than the above code suggests, because the code to initialize a new StringBuilder from a string has to copy the characters into place one at a time, and the append() method does the same.

The impact of this is considerable. Consider this method call:

        String intersectionType = ScanSupport.nextName(
            sc, "Intersection " + name + " ???"
        );

In the above, the total cost of the "nextName()" method may be less than the cost of the computation needed to compute the second parameter to the method call. This cries out for solution.

A First Solution

The first solution that comes to mind is to change the ScanSupport methods so that they take a number of strings as parameters. Thus, we replace this call:

        String intersectionType = ScanSupport.nextName(
            sc, "Intersection " + name + " ???"
        );

with this call:

        String intersectionType = ScanSupport.nextName(
            sc, "Intersection ", name, " ???"
        );

This means that the nextName() method must always receive 3 string parameters. In the normal case, nextName() ignores these parameters, but if there is a need to assemble an error message, it concatenates them. If you need an error message that involves fewer strings, just pass empty strings. If you need more, concatenate some of them or write an extra nextName method with more string parameters. With this solution, we still pay the cost of parameter passing, perhaps one or two instructions per string parameter, but the cost of concatenation is deferred until it is actually needed.

Of course, the number of parameters we need to pass depends on the particular error message we are generating. That's a nuisance. Also, for some of our error messages, the values we are passing aren't strings. Consider this line of code:

    ScanSupport.lineEnd(
        sc, "Road " + source.name + " " + destination.name + " " + travelTime
    );

Here, if we allow enough parameters, our new model would require something like this:

    ScanSupport.lineEnd(
        sc,
        "Road ", source.name, " ", destination.name, " ",
        travelTime.toString();
    );

We were forced to explicitly convert the floating point number to its textual form because the parameters were all strings. With string concatenation, Java automatically asks for the toString method when any non-floating object is concatenated, but when concatenation is not involved, we have to explicitly do the conversion. We could, of course, make a specialized form of scan-support routine that knows the format of the error message, including where all non-string parameters are used, but this would force the author of scan-support to know far more about the application. The scan-support package would then be very specific to one application instead of being a piece of code you can chop off and use for other purposes.

A General Solution

The most general solution involves replacing the data parameter with a parameter that conveys a computation. In Java, the way we do this is to contruct an object and pass that object. If the called routine needs the value, it will call a method of that object. That is the method that will do the work. Consider this new version of the ScanSupport.lineEnd() method:

public abstract class ErrorMessage {
    public abstract String myString();
}

static void lineEnd( Scanner sc, ErrorMessage message ) {
    String skip = sc.nextLine();
    if (!"".equals( skip )) {
        Errors.warning( message + message.myString() );
    }
}

Now, all we have to do to call our syntax-check method is first create a new subclass of ErrorMessage with the appropriate toString() method.

This sounds awful, but Java provides some shorthand to make it easy. We'll do the awful long-winded solution first before we look at the shorthand notation.

Note, we really wanted to use toString() as the name of the method above, but that doesn't work. You can't declare an abstract method in a Java class if it already inherits a concrete method from one of its superclasses, and all classes inherit toString() from class Class.