Brian's Waste of Time

Mon, 20 Mar 2006

Fooling the Java Compiler

I ran into a wall in Java's type system which it comes really close to supporting, then falls over. JDK 1.5 supports co-variant return types. This means that you can have a class, Foo, with a method, doStuff, which returns a Bar; and a class SubFoo which extends Foo and overrides doStauff with a return type of SubBar, which extends Bar. It's actually awfully handy.

I thought to make use of this in the implementation of Query and UpdateStatement in jDBI 2.0 to push all the parameter setting stuff up to a common super class (SQLStatement). Adding overrides for each explicitely type of argument is a pain, if I can do it once, bonus! It gets complicated as I like method chaining, so you can do query.bind("name", aString).bind("age", aDate).list() and whatnot. Of course, in this case, you need the specialized subclass you know you started with. Woot, covariant return types to the rescue!

But, er, wait. The child doesn't override the method, the method just returns this. Hmm, bugger. Next step is to try parameterizing the return type to return an instance of whatever it is parameterized to and push all the casts to one place:

class DefaultQuery<ResultType> 
         extends SQLStatement<DefaultQuery<ResultType>>
         implements Query<ResultType>
{
    ...
}

public abstract class SQLStatement>SelfType extends SQLStatement<
{
    @SuppressWarnings("unchecked")
    public SelfType bind(String name, Argument argument)
    {
        params.addNamed(name, argument);
        return (SelfType) this;
    }

    ...
}

What a hack! It does, however, work. Well, it is a hackjob so "work" makes more sense, right?

Sure enough, it belongs in quotes, and we get into the fun stuff! Did you notice that interface which DefaultQuery implements, Query? It has to define the methods in terms of Query, not a parameterized type (or it could, but then you get into utterly weird hackjobs). Funnily enough, this compiles perfectly fine! It even works for some invocations... for others it throws an abstract method error on a not only concrete, but final class which compiled happily.

So, yeah, it is a hackjob, but good lord, I didn't expect the compiler to be this easy to trick into generating bad bytecode. Woot! Will try all the 1.5 compilers I can find when I get off the plane, will be fun to see how different ones handle it. Other interesting part is if you are talking directly to the implementation, it works, if you are talking to the interface, kaboom.

Meanwhile, anyone have any ideas on how to factor out the parameter setting code in order to save thousands of lines of identical code except for the return type (which is always this) between queries, update statemements, and batch statement parts? I'm stymied, and really hate to cut, paste, and search-and-replace this, particularly as it means remembering to fix all the places when I make a bug fix. Argh.

Yes, dynamic typing solves this the easy and obvious way, but I have to believe there is some way in Java's bastardized static system to solve it as well!

update -- formatting of a solution option from peter royal

Recursive types? I had something similar I solved with recursive types...

i had:

interface Context {
	Context makeChildContext();
}                              

but I wanted it to return a strongly-typed child context when you have a concrete impl, that is, when you have a FooContext, I wanted it to return a FooContext. So, the solution with generics was:

interface Context<T extends Context<T>> {
	T makeChildContext();
}

then:

class FooContext extends Context {
	FooContext makeChildContext() {
		return new FooContext();
	}
}

1 writebacks [/src/java] permanent link