8. Improved input processing, Wrappers

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?

In the versin of the Road Network simulation we've developed, we put the collection of members of each class inside that class, and we had the class keep that collection private so that nobody outside the class could see how it was organized. The class exported an iterator so that the outsiders could look through the collection, but the collection itself was hidden. For example:

class Intersection {
    ... lots of details omitted ...

    // the collection of all instances
    private static final LinkedList <Intersection> allIntersections =
	new LinkedList <Intersection> ();

    /** Allow outsiders to iterate over all roads
     * @return iterator over roads
     */
    public static Iterator <Intersection> iterator() {
	return allIntersections.iterator();
    }
}

This allowed the main program to output the collection for debugging with the following code:

    for (Iterator<Intersection> i = Intersection.iterator(); i.hasNext(); ){
        System.out.println( "Intersection " + i.next() );
    }

In the case of intersections, we needed to provide a way to look up an intersection by name, so we added this method to class Intersection:

    public static Intersection lookup( String n ) {
	for (Intersection i: allIntersections) {
	    if (i.name.equals( n )) return i;
	}
	return null;
    }

This is really stupid code, a simple linear search of allIntersections for an intersection with the right name. Java provides much better tools for this! There are hash sets, for example. The problem here is that looking up these tools and taking the time to explore the options delays development. Writing a for loop to do a linear search is trivial, we did it without thinking. That was the right choice for early in the development process. Later, if we discover that this linear search is a bottleneck, we can replace the linked list with something allowing a faster search. This follows the classical rules of optimization:

In the case of a simulation program, building the model is usually very inexpensive compared to running the model. Our lookup method is only intended for use during building the model, so it will most likely never matter that it uses a stupid algorithm.

String Comparison

A reminder: The following code will most likely never work!

            if ((command == "intersection")

What's wrong with this? The problem is, Java offers several tools for comparison. The == operator is the simplest of these. In detail, the == operator has two distinct uses:

In the case of class String, each string constant such as "road" is created as a new object by the Java compiler before the time the program containing that constant begins to run. When a program calls an instance of class Scanner to read a string from some file, the scanner creates a new object of class String for each string it reads, without making any effort to see if a String object already exists that contains the same textual value. As a result, there may be a number of strings holding the same text that the == operator considers to be different strings.

Java didn't have to work this way. Class String could have maintained a dictionary of all strings encountered, so that whenever a new string is constructed, if that string was already in the dictionary, the existing dictionary entry would be used instead of creating a new object. That would have made string processing even slower than it already is, so the designers of the String class opted to make string construction as fast as they could without eliminating duplicates.

There are several alternative ways of comparing two strings a and b:

Note that there are two useful ways to compare string variables with string constants:

Using Intersection.lookup()

There were two places in our code with bug notices that we can now attack, in the initializers for the road and intersection classes. Consider the constructor for class Intersection:

    public Intersection( Scanner sc ) {
	name = sc.next();   // pick off name of intersection;
	// BUG:  Must Detect duplicate definitions of intersections?
	// BUG:  Intersection type?  Other attributes?  Handle this later.

	allIntersections.add( this );
    }

We need to look up the name of the intersection because our source file format only allows one definition of each named intersection. If an intersection has already been defined with this name, we ought to complain. Here is an initial suggestion for how to do this:

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

        name = sc.next();
        // Bug:  What if at end of file?  What does sc.next do?
        if (lookup( name ) != null) {
            Errors.warning( "Intersection " + name + " redefined." );
            // Bug:  Can we prevent the creation of this object?
        }
    }

We now have an initializer that complains with a useful error message when an attempt is made to declare a second (or third) intersection with the same name as a pre-existing intersection, but all may not be well. The new intersection is still created and added to the list of all intersections. Should we prevent this? Also, what if there is no next symbol for the scanner to scan? Our code is not prepared for this.

The second initializer we have to worry about is for roads. In this case, we want to guarantee that both intersections that the road connects are well defined. Here is the old code:

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

	... code to scan src, dst omitted ...
	
	destination = Intersection.lookup( dst );
	source = Intersection.lookup( dst );
	// BUG: What if lookup returns null!

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

What we need to do is look up both the source and destination roads and complain if either are undefined. Reasonable error messages should identify the road that caused the problem and also explain the error, so our improved code gets a bit long-winded:

	destination = Intersection.lookup( dst );
	source = Intersection.lookup( dst );

        if (source == null) {
            Errors.warning(
                "In road " + src + " " + dst +
                ", Intersection " + src + " undefined"
            );
            // Bug:  Can we prevent creation of this object?
        }
        if (destination == null) {
            Errors.warning(
                "In road " + src + " " + dst +
                ", Intersection " + dst + " undefined"
            );
            // Bug:  Can we prevent creation of this object?
        }

As with the previous example, we have not addressed what should be done if the object declaration contains an error, we have merely detected and reported the error. And again, we have not dealt with the possiblity that there might not be a source or destination intersection name.

The duplicated code above suggests that we might want to package the test for undefined names in a subroutine of some kind, perhaps a utility method for syntax checking that gets a name and checks to see if it is defined.

We've also inserted a new bug notice because we just noticed that there may be no sc.next() when we are trying to scan an intersection name. This also suggests we might want to package a more elaborate bit of code into a subroutine of some kind that not only scans an intersection name but checks to see if there is one.

Where were we?

Look what happened to our constructor to class Road:

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

	if (sc.hasNext()) {
	    src = sc.next();
	} else {
	    Error.warn( "road source missing\n" );
	    src = "???";
	}
	if (sc.hasNext()) {
	    dst = sc.next();
	} else {
	    Error.warn( "road " + src + " to missing destination\n" );
	    dst = "???";
	}
	if (sc.hasNextFloat()) {
	    travelTime = sc.nextFloat();
	} else {
	    Error.warn( "road " + src + " " + dst + " missing travel time\n" );
	    travelTime = Float.NaN;
	}
	destination = Intersection.lookup( dst );
	source = Intersection.lookup( dst );

        if (source == null) {
            Errors.warning(
                "In road " + src + " " + dst +
                ", Intersection " + src + " undefined"
            );
            // Bug:  Can we prevent creation of this object?
        }
        if (destination == null) {
            Errors.warning(
                "In road " + src + " " + dst +
                ", Intersection " + dst + " undefined"
            );
            // Bug:  Can we prevent creation of this object?
        }

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

It's huge! We need to find a way to shrink it, and it would be nice to find a way to prevent the road from being constructed if it is defective.

Another Utility class?

Again and again, throughout our program, we wrote code like this:

	if (sc.hasNext()) {
	    dst = sc.next();
	} else {
	    Error.warn( "road " + src + " to missing destination\n" );
	    dst = "???";
	}

What we really want to write looks more like this:

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

Where getNext is a method we wish we could add to class Scanner with the following semantics:

    public String getNext( String default, String msg ) {
        if (this.hasNext() ) return next();
	Errors.warn( msg );
        return default;
    }

If we had our way, we could declare a new class:

class myScanner extends Scanner {

    public String getNext( String default, String msg ) {
        if (this.hasNext() ) return next();
	Errors.warn( msg );
        return default;
    }

    public Float getNextFloat( Float default, String msg ) {
        if (this.hasNextFloat() ) return nextFloat();
	Errors.warn( msg );
        return default;
    }

Unfortunately, Java forbids us to do this! Class Scanner is a final class. That makes it illegal to extend the class with a subclass. As a result, we need to create what is called a wrapper or adapter class that presents the scanner operations we want in terms of the scanner operations we have

Wrapper or Adapter Classes

Here is a wrapper class for scanners:

class myScanner {
    Scanner self; // each myScanner wraps a scanner

    public boolean hasNext() {
        return self.hasNext();
    }

    public String next() {
        return self.next();
    }

    public String getNext( String default, String msg ) {
        if (this.hasNext() ) return next();
	Errors.warn( msg );
        return default;
    }
}

Wrappers are extremely common! Java provides class Integer as a wrapper for the type int because the latter primitive type is not really a class. Then Java does everything it can to prevent programmers from knowing that it did this by mechansims called autoboxing and unboxing so that, if you use an int where an Integer is expected, it is automatically boxed up in a wrapper object, and if you use an Integer where an int was expected, it is automatically unboxed.

Another common wrapper is the Java class File. The underlying operating system has files, with a file interface that does much of what Java's File objects do, but the interface is not object-oriented, or to the extent it is, it does not match Java's object model. To deal with this, the Java designers created class File, where every file object is "wrapped around" an open file in the underlying operating system.

Wrapper or interface classes are very commonly used to make portable code. The window managers of most modern systems are at least somewhat object-oriented, but they are incompatible. Window libraries on languages like Java exist as wrappers for the lower level window manager.

In terms of virtual machine hierarchies, wrapper classes are transparent virtual machine layers inserted between a lower level system and its users. The wrapper does not prevent using the lower level primitives, all it does is re-package them for higher level code.