Wednesday, August 09, 2006

Crossing generics and covariant returns

Quick: why doesn't this compile?

fred.setFoo(new Foo());

Ok, some context:

class Bob<Foo extends Foo> {
    private Foo foo;

    public void setFoo(final Foo foo) {
        this.foo = foo;
    }
}

class Fred extends Bob<Bar> {
}

See it now? How about now:

class Foo {
}

class Bar extends Foo {
}

class Bob<NOTFOO extends Foo> {
    private NOTFOO foo;

    public void setFoo(final NOTFOO foo) {
        this.foo = foo;
    }
}

class Fred extends Bob<Bar> {
}

Yes, it was a mean trick to name a generic parameter the same as the class it extends. Sensibly, SUN's javac complains "illegal forward reference". I had to reread several times the code this example came from before it finally clicked what had happened.

But enough; on to the meat of the post.

I was cleaning some code and noticed this mini-anti-pattern in many places:

class Bob {
    private Foo foo;

    public Foo getFoo() {
        return foo;
    }

    public void setFoo(final Foo foo) {
        this.foo = foo;
    }
}

class Fred extends Bob {
}

class SomewhereElse {
    public void doWork(final Fred fred) {
        final Bar bar = (Bar) fred.getFoo();
    }
}

This style is quite commmon in pre-generics Java where there are parallel inheritance hierarchies: Foo goes with Bob, Bar goes with Fred, et al, and all the common code is pushed down into the lowest base class. All that casting makes my eyes sore.

But using generics, I get the advantages of covariant returns without needing any extra coding:

class Bob<F extends Foo> {
    private F foo;

    public F getFoo() {
        return foo;
    }

    public void setFoo(final F foo) {
        this.foo = foo;
    }
}

class Fred extends Bob<Bar> {
}

Now the syntax error I first posed is a real time-saver: instead of getting a ClassCastException at runtime, I get a compile error when trying:

fred.setFoo(new Foo());

The correct call is now:

fred.setFoo(new Bar());

What are covariant returns? To get generic code like this to work right, Java picked up covariant returns for free:

class abstract NumberPicker {
    public abstract Number pickANumber();
}

class IntegerPicker extends Base {
    public Integer pickANumber() {
        return 42;
    }
}

Because an Integer is a Number, Java recognizes that declaring IntegerPicker to return an Integer for pickANumber() satisfies the contract for NumberPicker: covariant returns. Generics uses the same trick in the compile when narrowing the return type for getFoo() in the first examples, or something near enough.

You can read more on this and other goodies in Angelika Langer's Java Generics FAQs.

No comments: