Tuesday, January 31, 2006

More on Generics

In a recent discussion on the Slurp list, Sun's pages on advanced use of generics came under some scrutiny.

Sun says:

Suppose we want to create a TreeSet<String> and pass in a suitable comparator, We need to pass it a Comparator that can compare Strings. This can be done by a Comparator<String>, but a Comparator<Object> will do just as well.

Slurp say:

We think introducing Object into a discussion about generics is bad... It would be better if the page had avoided Strings and Objects, and said:

"Suppose we want to create a TreeSet<Chicken> and pass in a suitable comparator. We need to pass it a Comparator that can compare Chickens. This can be done by a Comparator<Chicken>, but a Comparator<Animal> (where Animal is a superclass of Chicken) will do just as well. We COULD write the constructor as TreeSet(Comparator<? extends Animal> c) but it would be better to avoid mentioning a specific superclass of Chicken. Therefore TreeSet(Comparator<? super Chicken> c) is best."

Comparator<? extends Animal> is not quite good enough, as I can pass in a FreeRangeChickenComparator() which ranks chickens based on their happiness, an attribute not exposed in the Chicken superclass, but this is not really the point here. I agree that what Sun's page says is confusing. In most cases, a Comparator<Object> will not do "just as well", as it will likely return a different ordering.

If you're just using the TreeSet as an efficient storage structure then any order will indeed do, but if you want to iterate over the contents, then you likely want a particular total ordering, e.g. alphabetically by species, or average egg diameter...

TreeSets in general are slightly annoying, as they try to combine being a TreeSet<E extends Comparable>, with being a TreeSet<E> with a constructor taking a Comparator<? super E> (as we have seen). In order to support passing in a custom Comparator (which could work on any type), the set must be declared as TreeSet<E>, rather than TreeSet<E extends Comparable>.

This leads to a few loopholes. I can easily create a TreeSet<NotComparable>, where upon calling treeSet.add(new NotComparable()); (more than once, as you don't need to compare anything when you add the first element) causes a runtime ClassCastException. So this test compiles and passes:

public void testGenericTreeSet() {

try {
Set<NotComparable> ts = new TreeSet<NotComparable>();
ts.add(new NotComparable());
ts.add(new NotComparable());
fail();
} catch(Throwable t) {
assertThat(t, isA(ClassCastException.class));
}
}
}

Generics are not being very helpful here, you'd hope that all this extra "type-safety" would allow this sort of thing to be flagged up at compile time, but making this class work in two ways, and in keeping everything backwardly compatible, there are some cracks between the floorboards.





<< Home

This page is powered by Blogger. Isn't yours?