9. Errors, Organization, and Scaffolding

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

 

Where were we?

Something Testable

The code we have reads a file describing a road network, so our initial test can be code that reads this file and then writes it out. If the program is correct, the output should resemble the input.

Resemble, not be exactly the same as. Why? Because the tools in class Scanner squeeze out extra spaces and because, depending on how the members of class LinkedList are handled, the order of roads and intersections may not be the same.

Every Java object has a toString() method that converts that object to a textual representation as a character string. The default method creates a near-useless representation, but we can easily override the default. For example, here is an appropriate toString() method for our class Road:

    public String toString() {
        return (
            source.name + " " +
            destination.name + " " +
            travelTime
        );
    }

Assuming that we write similar code for class Intersection, the main program can be modified to call writeNetwork() after it calls readNetwork(). The obvious thing to do is to make writeNetwork() be a static method of RoadNetwork, just like readNetwork().

If each class maintains a privat list of all class members, we face the problem in writeNetwork() of how to iterate over all roads. To solve this, we add a static iterator() method to each class that returns the iterator for that class. For example:

    /** Allow outsiders to iterate over all roads
     * @return textual name and travel time of the road
     */
    public static Iterator  iterator() {
        return allRoads.iterator();
    }

In the context of the above, the writeNetwork() code will look something like this:

    /** Print out the road network from the data structure
     */
    static void writeNetwork() {
        for (Iterator i = Intersection.iterator(); i.hasNext();) {
            System.out.println( "Intersection " + i.next().toString );
        }
        {
            Iterator  i = Road.iterator();
            while (i.hasNext()) System.out.println( "road " + i.next() );
        }
    }

The above code illustrates two ways of doing exactly the same thing. It is quite likely that the Java compiler generates exactly the same J-code for both of these because all forms of the Java for loop are exactly equivalet to versions that just use a while loop. Programming language designers sometimes refer to the for loop as "syntactic sugar" because it does not really change the expressiveness of the language, it just provides an alternative way to write something that the language can already do without it.

Also notice that when + is used for string concatenation in Java, it will automatically look for the toString() method of its right-hand operand when that operand is not already of class String.

If the list of all roads was a public component of class Road we could have written this:

        For (Road r: Road.allRoads) {
            System.out.println( "road " + r );
        }

This notation is even more compact, but it is simply an even more compact shorthand for code involving a while loop:

        {
            Iterator  i = Road.allRoads.iterator();
            while (i.hasNext()) {
                road r = i.next();
                System.out.println( "road " + r.toString() );
            }
        }

The disadvantage of this is that it exposes the list to outsiders. This means that code outside class Road is no able to know that we used a linked list of raods for the collection, and the code is able to perform arbitrary deletions or reorderings to the list. This violates the general rule that no software component should have access to more information than it needs to do its job.

As a general rule, the for-loop construct in Java is always a shorthand. Every Java for loop can be rewritten as a while loop. Consider this elementary for loop that iterates over integers:

for(int i=0 ; i < 10 ; i++) {
    doSomethingWith( i );
}

This can be rewritten as follows, and in fact, the Java compiler would generate exactly the same code from the above as it generates from this long-winded rewrite:

{
    int i = 0;
    while (i < 10) {
        doSomethingWith( i );
        i++;
    }
}

Again, we wrapped the while loop in an extra set of braces in order to make the loop control variable i visible only inside the loop and not elsewhere in the program.

Errors

There are some obvious ways to deal with errors! We could throw an exception, for example, but Java demands that you write handlers for exceptions, so this doesn't get us anywhere.

An easy alternative is to call System.exit( 1 ). This terminates the program rather violently. By convention, the integer parameter to exit() should be zero if the program is terminating normally, while it should be nonzero in case of abnormal termination. The exit is violent, in the sense that all kinds of things are abandoned -- finalization on objects is ignored, for example. The convention that the parameter should be zero for success and nonzero for failure is a standard inherited from Unix and Linux; it only really matters when programs are called from elaborate shell scripts or Perl scripts.

Of course, before calling exit(), we should output an error message! Consider the following rewrite of the start of the main method:

    public static void main(String[] args) {
        // open the input file from args[0]
        if (args.length < 1) {
            System.err.println( "Missing filename argument" );
            System.exit( 1 );
        } else if (args.length > 1) {
            System.err.println( "Unexpected extra arguments" );
            System.exit( 1 );
        } else ...

First note, instead of outputting error messages to System.out, the standard output stream, we have output the error messages to System.err. All processes on systems descended from UNIX have two default output streams, one for normal output, and the other for error messages. Linux and Windows both support this idea. System documentation usually refers to these streams as stdout and stderr, but in Java, they are called System.out and System.err.

This approach to dealing with errors leads us to rather verbose code, and it locks us in by distributing error reporting code all over the program. What if we want to add a GUI interface to this application? If we proceed as suggested above, we'll have to inspect the entire program to find all places where error messages are output and modify all of them to user our new GUI. With a GUI, we probably don't want to just output a message and then immediately kill the applicaton. If we did that, all the open windows attached to the application would close with a bang, including wherever the error message appeared. Instead, we want to open a popup window and have it hang there until the user has read it. We certainly don't want to duplicate all the code for that everywhere an error message is output.

What we need to do to avoid both of these problems is provide a centralized error handler, for example, something like this:

/** Error reporting framework
 */
class Errors {
    static void fatal( String: message ) {
        System.err.println( message );
        System.exit( 1 );
    }
}

Later, if someone wants to convert our program to use a GUI, this one method could be responsible for opening a popup window to report the error and then wait for the user acknowledgement. Later, we might also add non-fatal error dialogues, and this class is a natural place to put them. Our main method would now begin like this:

    public static void main(String[] args) {
        if (args.length < 1) {
            Errors.fatal( "Missing filename argument" );
        } else if (args.length > 1) {
            Errors.fatal( "Unexpected extra arguments" );
        } else try {
            readNetwork( new Scanner( new File( args[0] ) ) );
            writeNetwork( new Scanner( new File( args[0] ) ) );
        } catch (FileNotFoundException e) {
            Errors.fatal( "Can't open file '" + args[0] + "'" );
        }
    }

When it comes time to report less serious errors, we could add a warning() method to class Errors that works much the same as the fatal() method except that it does not exit.

We can call this, for example, in readNetwork():

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

What we have done here is build a new layer on top of the virtual machine hierarchy supporting our program. This layer sits on top of the Java library and below our main program. It extends our programming environment by adding a uniform error handling mechanism. As we further develop our program, we will add other supporting mechanisms to this virtual machine.

Static methods

In Java, all subroutines are described as methods (the term subroutine being the most generic of all names for chunks of executable code). Java methods come in three flavors, regular methods, static methods and constructors.

Regular methods — true methods — must be called relative to an object. For example, System.out is the name of an object, an output stream, and println() is a method of the stream class. The call System.out.println() calls the println() method in the context of the object System.out. True methods cannot be called except in the context of a particular object.

Static methods such as Errors.fatal() do not have an object as their context. The only variables they can operate on are their parameters and static variables -- that is, variables that are not part of an object. Prior to the invention of object-oriented programming, the term method was not used. The term subroutine comes from FORTRAN in the 1950s. The term procedure comes from Algol. In both languages, a function is a kind of procedure or subroutine that returns a value. All of these can be translated to Java as static functions.

Java constructors are a special case. To a large extent, they are analogous to static methods except that they also operate on newly allocated object that they implicitly return, without the need for a return statement.

Where were we?

Look at this code:

class Road {
    float travelTime;               // measured in seconds
    Intersection destination;       // where road goes
    Intersection source;            // where road comes from
    // name of road is source-destination

    // constructor
    public Road( Scanner sc, LinkedList  inters ) {
        // code here must scan & process the road definition

        String srcName = sc.next();
        String dstName = sc.next();
        // Bug:  Must look up srcName to find source
        // Bug:  Must look up dstName to find destination

        // Bug:  What if no next float?
        travelTime = sc.nextFloat();
        String skip = sc.nextLine();
    }

    // other methods
    public String toString() {
        return (
            sourceName + " " +
            dstName + " " +
            travelTime
        );
    }
}

Visibility

Is source.name legal here? It is outside class Intersection. This raises the question, what fields ought to be visible, and if visible, how can we protect them.

There is a rule of thumb in programming that comes from military security: The need to know rule. This states that no user of an object should be given more information about the internal state of that object than they need to know to get their job done.

Need to know security goes against the principle of transparency, that in an open society, secrets are bad. The primary reason that we keep secrets in object-oriented programming is to minimize the size of the public interface of each class, but keeping secrets also has genuine security consequences. In a large project, a rogue programmer working on one part of the program can harvest any information about the system that is public and export it. By only giving each programmer access to the bare minimum information needed to do the job, we limit the damage a rogue programmer could do and maximize the likelihood that rogues behavior will be detected.

In Java, each declaration can be marked as: public, private, or final. In general, marking declarations as private makes very good sense. It means that the variable is invisible to all code outside this class.

If a variable must be visible because outsiders need to know about it, you can minimize the damage they can do by marking it final. Final variables are not read-only, their values can be changed, but the binding of the variable name to the object is fixed for all time. Declare variables to be public only if outsiders have a natural need to not only see the variable but to make arbitrary changes to it.

Later, when we discuss clas hierarchies, we will discuss an additional attribute, protected. Protected variables are like private variables in programs that do not use class hierarchies. In a class hierarchy, one class may be a subclass of another. In programs that use class hierarchies, protected variables are visible not only in the class where they are declared, but also in all subclasses of that class. For the moment, we can ignore this marking.

In our running example road network code, the following declarations make sense:

class Road {
    private float travelTime;        // measured in seconds
    private Intersection destination;// where road goes
    private Intersection source;     // where road comes from

    ...
}

class Intersection {
    public final String name;
    private LinkedList  outgoing = new LinkedList  ();
    private LinkedList  incoming = new LinkedList  ();

    ...

The markings above make all fields private except for the name of an intersection, which is final and public. Note that there must be exactly one assignment to a vinal variable in the constructor for a class. Java compilers enforce this rule conservatively, which means that there are cases where you, as a programmer, can assure that there is exactly one assignment to a variable but the Java compiler is not smart enough to figure this out. In such cases, the compiler will not permit that variable to be declared as final.

Lookup

The next problem we face is that of looking up each intersection. The need for this is announced by bug notices in our code:

class Road {

    ...

    // constructor
    public Road( Scanner sc, LinkedList  inters ) {
        // code here must scan & process the road definition

        String sourceName = sc.next();
        String dstName = sc.next();
        // Bug:  Must look up sourceName to find source
        // Bug:  Must look up dstName to find destination
        ...
    }
    ...
}

class Intersection {

    ...

    // constructor
    public Intersection( Scanner sc, LinkedList  inters ) {
        // code here must scan & process the intersection definition

        name = sc.next();
        // Bug: look up name!
      
        ...
    }
    ...
}

Obviously, we need a way, given the name of an intersection, to find that intersection in the list of all intersections. Since this list is a static field of class RoadNetwork, one way to do the lookup is to have RoadNetwork export a public static method to look things up.

public class RoadNetwork {
    static LinkedList <Intersection> inters
        = new LinkedList <Intersection> ();

    ...

    static Intersection findIntersection( String s ) {
        // return the intersection named s, or null if no such
        // Bug: flesh this out!
    }

    ...
}

The obvious way to code this is with a rather stupid linear search through the list of roads to find the right one. Java includes some far more sophisticated tools for this search, but we'll ignore them here. One very sound rule of software engineering is: