16

If I create an arbitrary class that does not implement Comparable, and try to use it as a treeset, it throws an exception at run time when an object is inserted:

public class Foo {
}

public TreeSet<Foo> fooSet = new TreeSet<Foo>();
fooSet.add(new Foo()); // Throws a ClassCastException exception here: Foo is not comparable

I'm no Java expert, but something about this seemed dynamically typed (ala Python) in a way I wasn't expecting. Is there no way for TreeSet's implementation to specify that its generic type argument must implement Comparable so that this can be caught at compile-time? Non-generic functions can take interfaces as arguments; is the same not possible with generics?

uscjeremy
  • 348
  • 1
  • 5
  • 12

2 Answers2

33

TreeSet is implemented that way because you can alternatively provide a Comparator, in which case the elements don't need to be Comparable. The only way to support both behaviors without splitting the implementation into multiple classes was to include runtime checks - this was simply a design decision by the author(s) of that class.

Exposing factory methods for TreeSet instead of public constructors would've been a way to maintain compile time checks using stricter generic type constraints, but that would've been a break from the core collections API's convention of exposing public no-arg and copy constructors for its implementation classes. As you noted in your comment, Guava goes the factory route with its collections and IMHO is better off for it.

Paul Bellora
  • 54,340
  • 18
  • 130
  • 181
  • 2
    Thank you - this makes perfect sense. In fact, I note that Google's Guava library has a [TreeMultiSet](http://google-collections.googlecode.com/svn/trunk/javadoc/com/google/common/collect/TreeMultiset.html#create(java.util.Comparator)) class which is designed just as you describe - with two factories, one of which requires the generic extend Comparable, and one of which takes an appropriately typed Comparator. I guess they decided to fix the design "bug" to bring more checks out to compile time. – uscjeremy Dec 15 '12 at 08:31
  • 2
    Yeah, it's especially troublesome that `TreeSet` can't fail until you actually `put` an element that doesn't work, thanks to type erasure. Luckily it's at least a "fool me once" kind of failure, but still annoying not to catch it at compile time. – Paul Bellora Dec 15 '12 at 08:36
  • 3
    I think it's actually worse -- if I had a bunch of subclasses of `Foo`, and only some of them implemented Comparable, `add` won't throw an exception until one of the non-Comparables is finally inserted. Who knows when that will finally happen? This is a frustrating design choice: I use Java over dynamically typed languages partly to get away from the chaos of not having type checking until every code path has executed! – uscjeremy Dec 15 '12 at 08:45
  • But why does the *Collection.sort(list)* method has such a compile time check? For the single argument function, compiler shows error when my class is not implementing *Comparable*. – Sreehari S Jul 17 '18 at 10:51
  • @SreehariS Because that method is specific to elements that are Comparable, rather than being in a dual-role like `TreeSet` (Comparable elements, or has-Comparator). – Paul Bellora Jul 17 '18 at 14:32
  • @PaulBellora For *Collections.sort()* also, it can have a dual role as in *public static void sort(List list, Comparator super T> c)* , right? – Sreehari S Jul 18 '18 at 09:44
  • 1
    @SreehariS That's a separate method that declares different type constraints. `TreeSet` is a single type that must declare one set of type constraints for both scenarios. – Paul Bellora Jul 18 '18 at 21:38
0

You can make a type-safe wrapper for TreeSet:

public class TypeSafeTreeSet<T extends Comparable<? super T>> extends TreeSet<T> {
}
Ivan
  • 87
  • 1
  • 6