12. Abstract Classe, Inner Classes, Anonymity

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

 

Preventing a Constructor from Constructing

Here is the constructor for class Road we currently have:

    public Road( MyScanner sc ) {
        // keyword Road was already scanned
        final String src;	// where does it come from
        final String dst;	// where does it go

        src = sc.getNext( "???", "road source missing" );
        dst = sc.getNext( "???", "road " + src + " to missing destination" );
        travelTime = sc.getNextFloat(
            Float.NaN, "road " + src + " to missing destination"
        );
        destination = Intersection.lookup( dst );
        if (destination == null) {
            Error.warn( "road " + src + " " + dst + " undefined: " + dst );
        }
        source = Intersection.lookup( src );
        if (source == null) {
            Error.warn( "road " + src + " " + dst + " undefined: " + src );
        }
        // BUG: Can we prevent creation of malformed roads (see toString bug)

        allRoads.add( this ); // this is the only place items are added!
    }

One way to handle errors in a constructor -- that is, to prevent the constructor from returning a defective object, is to have the constructor throw an exception. We could throw some generic exception, just search through the list of exceptions that Java has already defined and find the one that comes closest, but this is not a very satisfying soution because most of them are obviously special purpose exceptions for some other domanin. As an alternative, we can define a new subclass of exceptions. Consider doing this in class Road in our running example:

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

    ...

    public Road( MyScanner sc ) throws ConstructorFailure {
        // keyword Road was already scanned
        final String src;	// where does it come from
        final String dst;	// where does it go

        src = sc.getNext( "???", "road source missing" );
        dst = sc.getNext( "???", "road " + src + " to missing destination" );
        travelTime = sc.getNextFloat(
            Float.NaN, "road " + src + " to missing destination"
        );
        if ((src == "???") || (dst == "???") || Float.isNaN( travelTime )) {
            // this takes care of the errors detected above
            throw new ConstructorFailure();
        }
        destination = Intersection.lookup( dst );
        if (destination == null) {
            Error.warn( "road " + src + " " + dst + " undefined: " + dst );
            throw new ConstructorFailure();
        }
        source = Intersection.lookup( src );
        if (source == null) {
            Error.warn( "road " + src + " " + dst + " undefined: " + src );
            throw new ConstructorFailure();
        }
        if (travelTime < 0) {
            Error.warn( this.toString() + ": negative travel time?" );
            throw new ConstructorFailure();
        }

        allRoads.add( this ); // this is the only place items are added!
    }

Now, the constructor will not return a newly constructed road if that road is defective, and the defective road will not be added to the list of all roads. Instead, it will throw an exception, and the calling code will have to deal with that exception.

One line in the above raises a number of questions:

        if ((src == "???") || (dst == "???") || Float.isNaN( travelTime )) {

First, why didn't we use the "???".equals(src) construction? The answer to this is that the Java compiler collects all of the string constants used in a program and makes sure that just a single constant object is created for each distinct string. As a result, no matter how many times the string constant "???" show up in the program, every single use of this constant is a reference to exactly the same object. As a result, using the == operator in this case will return true if and only if the values on both sides come from the string constant "???" and not from any other source.

In fact, because of the way string constants are handled, if the scanner finds the string ??? in the user input and that ends up in src, Java guarantees that src=="???" will be false, while "???".equals(src) would have been true. If our goal is to detect use of the default constants pased to the get methods of class MyScanner, src=="???" works better because while "???".equals(src) can also match text from the input file.

The second question is, why Float.isNaN(travelTime)) instead of something more readable like travelTime==Float.isNaN. The problem here is that not-a-number is a weird value. The IEEE floating point standard, which Java obeys, requires that if x is not a number, then the comparison x==y for all values of y should return false. This means that even x==x will be false when x is not a number. This means, paridoxically, that we could have written travelTime!=travelTime to detect that the value is not a number. That works, but it's not readable.

Looking in the definition of classes Float and Double, we find two solutions: travelTime.isNan(), a method that applies to an object of the class, and Float.isNaN(travelTime), a static method that takes an instance of the class as a parameter. Why does Java allow both? Wouldn't one suffice?

The answer to the above question has to do with the difference between float (lower case) and Float (upper case). The primitive data type float is not a class. Variables of this type are not objects, arithmetic on these variables is fast, being done by machine hardware. But, because float is not a class, you can't create things like a LinkedList of float values.

In contrast, Float is a real class. Eacn Float object can be thought of as a box around a float value. You can create a LinkedList of Float values, and you can apply all kinds of useful methods to those values, such as isNaN().

To help naive users from having to know about all this, Java has a feature called autoboxing. If f is a float value and it is used in a context where a Float is expected, for example f.isNan(). The Java compiler automatically "boxes up f" as a Float creating code equivalent to new Float(f).isNan. This is called autoboxing.

Similarly, if g is a Float and it is used in a context where a float is expected, such as g+1.0F the compiler unboxes it, compiling it as if it had been written g.floatValue()+1.0. This is called auto-unboxing. We used Float.isNaN(travelTime) above in order to avoid the computational cost of autoboxing.

If you look at the methods of Boolean, Integer, Real and Double you will find many pairs of methods like f.isNan() and Float.isNaN(f). These are all there so that the programmer can avoid the cost of boxing and unboxing arguments depending on whether the value being operated on is an object or one of the primitive machine data types.

Classes that are Never Instantiated

There's a problem with our class Error and RoadNetwork. As the code stands, you can write new Error(), at which point, Java will happily create a new object of class Error, despite the fact that this is a useless object.

If we declare the class as abstract, we prevent this error. It is still legal, however, to create (useless) subclasses of these classes. We can also define a class to be final which prevents anyone from ever creating a subclass of that class, but unfortunately, Java does not permit the combination of abstract and final.

When we wrote Errors and RoadNetwork, we said that no instances of these classes would ever be created. We can enforce this by declaring them to be abstract as well. So, for example, we ought to write:

/**
 * Error handling
 */
abstract class Error{
    private static int errorCount = 0;
    private static final int maxErrors = 10;

    public static void warn( String message ) {
        System.err.println( message + "\n" );
        errorCount = errorCount + 1;
        if (errorCount > maxErrors) System.exit( 1 );
    }

    public static void fatal( String message ) {
        warn( message );
        System.exit( 1 );
    }
}

In the above, we also made the error handler limit count the errors and limit the count to a reasonable value. We declared the limit on the error count to be a final int which makes it effectively constant.

Note that final does not make objects constant unless they are immutable. It finalizes the identity of the object referenced to a variable. Some objects are immutable. Class String is an example, because there are no methods to change the value of an object in that class, only methods that construct new objects to hold a new value. Class LinkedList is an example of a mutable class. A single list object may be manipulated in a wide variety of ways.

Final Variables and Class Hierarchy

Now, let's return to class Intersection. We declared the name of an intersection to be final, like this:

abstract class Intersection {
    public final String name;   // textual name of intersection, never null!

We were forced to add a protected constructor to the class so that the constructors in the subclasses could initialize name. If a Final variable is not set on the line where it is declared, it must be set in the constructor for the class itself, not in any subclass.

It helps to understand 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.

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.

Wasted computation

Look at the code we currently have to construct a new road:

    public Road( MyScanner sc ) throws ConstructorFailure {
        // keyword Road was already scanned
        final String src;	// where does it come from
        final String dst;	// where does it go

        src = sc.getNext( "???", "road source missing" );
        dst = sc.getNext( "???", "road " + src + " to missing destination" );
        travelTime = sc.getNextFloat(
            Float.NaN, "road " + src + " to missing destination"
        );
        if ((src == "???") || (dst == "???") || travelTime.isNaN()) {
            // this takes care of the errors detected above
            throw new ConstructorFailure();
        }
        destination = Intersection.lookup( dst );
        if (destination == null) {
            Error.warn( "road " + src + " " + dst + " undefined: " + dst );
            throw new ConstructorFailure();
        }
        source = Intersection.lookup( src );
        if (source == null) {
            Error.warn( "road " + src + " " + dst + " undefined: " + src );
            throw new ConstructorFailure();
        }
        if (travelTime < 0) {
            Error.warn( this.toString() + ": negative travel time?" );
            throw new ConstructorFailure();
        }

        allRoads.add( this ); // this is the only place items are added!
    }

Specifically, look at these lines:

        dst = sc.getNext( "???", "road " + src + " to missing destination" );

        travelTime = sc.getNextFloat(
            Float.NaN, "road " + src + " to missing destination"
        );
In these lines 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:

        dst = sc.getNext( "???", "road " + src + " to missing destination" );

This is really equivalent to

        {
            StringBuilder s = new StringBuilder( "road " );
            s.append( src );
            s.append( " to missing destination" );
            dst = sc.getNext( "???", s.toString() );
        }

Many of the methods on class StringBuilder end with return this. As a result, the above can be rewritten in a more compact form:

        {
            StringBuilder s = new StringBuilder( "road " )
                                .append( src )
                                .append( " to missing destination" );
            dst = sc.getNext( "???", s.toString() );
        }

We can even rewrite this as:

        dst = sc.getNext(
	    "???",
	    new StringBuilder( "road " )
                    .append( src )
                        .append( " to missing destination" )
                            .toString()
        );

None of the above rewrites reduce the number of method calls or the amount of string copying, where each string is represented internally as an array of characters, and string copying involves copying each of these characters. As a result, the total cost of the call to getNext() 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.