17. Inner Classes, Interfaces & Anonymity
Part of
CS:2820 Object Oriented Software Development Notes, Spring 2019
|
If we look at the code in the current version of the code, 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.
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.
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.
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.