From the Trenches

I’m still working on the API improvements to support annotations with subtyping. It turned out there is much more to do than I thought: Reading the class files, parsing the annotations and then creating the corresponding runtime objects is quite a bit of work, not difficult but tedious. I still have to deal with annotations as members, and with arrays of annotations and arrays of arrays. Additionally, I have to read out the default values of annotation members; right now, if a member isn’t specified, its value is null.

The high level parts of my framework make extensive use of the Visitor and Template Method design patterns. Unfortunately, I made a mistake again that I’ve made often, and that I’ve pointed out very often too. Consider the visitor interface and the abstract class implementing it:

interface IVisitor {
public R intCase(IntLeaf host, P param);
public R addCase(AddNode host, P param);
public R mulCase(MulNode host, P param);
}
abstract class ADefaultVisitor {
public abstract R defaultCase(INode host, P param);
public R intCase(IntLeaf host, P param) {
return defaultCase(host, param);
}
public R addCase(AddNode host, P param) {
return defaultCase(host, param);
}
public R mulCase(MulNode host, P param) {
return defaultCase(host, param);
}
}

The abstract class implements a very simple Template Method pattern: It adds an abstract method that needs to be implemented, and then implements all the methods from the interface so that they call the new method. A default visitor like this is useful when most cases of a visitor are treated the same, e.g. during error checking when only one case is acceptable and all other cases should throw an exception:

class LeafOnlyVisitor {
public Void defaultCase(INode host, Void param) {
// called by addCase and mulCase
throw new RuntimeException("leaf expected");
}
public Void intCase(IntLeaf host, Void param) {
return Void.TYPE;
}
}

Unfortunately, it’s very easy to accidentally change the signature of the method that we’re trying to override, and therefore not override the method but add another method, one that is never called. In the first of the two examples below, the method name is misspelled; in the second, the type of the first parameter is different.

class LeafOnlyVisitorFlawed {
public Void defaultCase(INode host, Void param) {
// called by addCase and mulCase
throw new RuntimeException("leaf expected");
}
public Void untCase(IntLeaf host, Void param) {
return Void.TYPE;
}
}

class LeafOnlyVisitorFlawed2 {
public Void defaultCase(INode host, Void param) {
// called by addCase and mulCase
throw new RuntimeException("leaf expected");
}
public Void intCase(INode host, Void param) {
return Void.TYPE;
}
}

In both cases, there is no compiler error, because all methods from the interface and the method from the abstract class have been implemented, so it’s often difficult to discover this problem. I just realized how nice it would be to specify that subclasses of a class may only override methods, never add new methods. Using annotations, the syntax could look like this:

@OverrideOnly
abstract class ADefaultVisitor {
public abstract R defaultCase(INode host, P param);
public R intCase(IntLeaf host, P param) {
return defaultCase(host, param);
}
public R addCase(AddNode host, P param) {
return defaultCase(host, param);
}
public R mulCase(MulNode host, P param) {
return defaultCase(host, param);
}
}

When the classes LeafOnlyVisitorFlawed and LeafOnlyVisitorFlawed2 accidentally add a method instead of overriding one, an annotation-based checker could emit errors.

To allow better code reuse, it probably makes sense to allow private methods to be added to a class marked @OverrideOnly.

Share

About Mathias

Software development engineer. Principal developer of DrJava. Recent Ph.D. graduate from the Department of Computer Science at Rice University.
This entry was posted in Research, xajavac. Bookmark the permalink.

Leave a Reply