16. Abstract Classes and Methods

Part of CS:2820 Object Oriented Software Development Notes, Spring 2019
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.