Generics Journey Continues

Brain Explode
WARNING: Brain Might Explode

I’m not sure I will ever fully grok Java generics. Kevin’s explanation of Set.contains() was enough to prod me into well over 1 TimeUnit.HOURS of experimentation tonight.

While I can say that I follow his examples and could verify all of the behavior he describes, I cannot say I have a sense of deep confidence in this dark corner of the language.

What I Did

I wanted to experiment with different variations of Set.contains(...), but modifying the JDK Set interface would be quite complex. Instead, I wrote my own little interface:

public interface MySet<E> {
  boolean contains(Object o);
  boolean add(E o);
}

Along with a simple implementation (that merely delegates to the real HashSet), this is enough to tinker with the concepts.

Step 1: Verify Kevin’s Claims

As expected, the code below works exactly as he describes:

public class Main {
  public static void main(String[] args) {
    MySet<Long> set = new MyHashSet<Long>();
    set.add(10L);
    if (set.contains(10)) {
      System.out.println("10 is contained!");
    } else {
      System.out.println("10 is NOT contained!");
    }

    MySet<Foo> foos = new MyHashSet<Foo>();
    MySet<SubFoo> subFoos = new MyHashSet<SubFoo>();
    doSomeReading(foos);
    doSomeReading(subFoos);
  }

  public static void doSomeReading(MySet<? extends Foo> foos) {
    // EDIT: my first post incorrectly said new Foo() {}; here
    Foo aFoo = new Foo();
    System.out.println(foos.contains(aFoo));
  }
}

When you run the above code, it prints “10 is NOT contained!”, plus a few “false” messages from the dummy method.

Step 2: Modify Set.contains(…)

Now I tried changing my contains(...) method to this:

boolean contains(E o);

This breaks the sample program in two places. First, this line fails (which is actually good):

if (set.contains(10)) {

That’s nice because 10 is boxed into an Integer, and our Set expects a Long. Changing the parameter to 10L fixes that issue.

The second breakage is a problem, however:

System.out.println(foos.contains(aFoo));

I tried all sorts of mojo and came up with this ugly hack:

public static <T extends Foo> void doSomeReading(MySet<T> foos) {
  T aFoo = (T) new Foo();
  System.out.println(foos.contains(aFoo));
}

You know what? Everything compiles now. Only…with a warning:

Unchecked cast: ‘Foo’ to ‘T’

Damn.

Step 3: Try Something Else

So now I tried changing contains(...) to this:

boolean contains(? extends E o);

That won’t even compile. IDEA says “Wildcards may be used only as reference parameters”. Uh, yeah.

Step 4: Try Again

Then I tried this:

<T extends E> boolean contains(T o);

Hey, at least that compiles! As in my earlier attempt, the sample program fails to compile in the same two spots. I can fix the first issue by passing 10L, just like before.

And the same doSomeReading(...) hack also compiles, although with the same ugly warning as before.

Closing Thoughts

  • Given the way generics work today, contains(Object o) seems better than any alternative.
  • IDEA’s “Suspicious call to ‘Set.contains’” warning only works with the built-in collections. The warning does not fire for my custom MySet interface.
  • I have to wonder…is there some underlying language improvement that could simultaneously allow both strict contains(E o) signatures while also supporting the substitutability principle for collections?

One more thing…all this talk of adding closures to Java…that scares me to death. Spontaneous brain explosion is a serious concern, already affecting 3% of Java programmers. With closures added to mix, that number will undoubtedly rise.


6 Responses to “Generics Journey Continues”

Jesse Says:

Your example ever-so-slightly changes the contract for the contains() method. This code compiles with Set, but not with MySet. It’s reasonable code in both cases:
MySet collections = …
collections.add(Collections.singletonList(”A”));
Iterable a = Collections.singletonList(”A”);
collections.contains(a);

Bob Lee Says:

FWIW, this:

<T extends E> boolean contains(T o);

is equivalent to this:

boolean contains(E o);

i.e. I can already pass in any sub type of E.

“new Foo() {}” effectively creates a new anonymous sub type of Foo which does not extend SubFoo. The compiler can’t let us pass a “new Foo() {}” to a method which expects a SubFoo. Incidentally, it can’t let us pass in a plain old Foo to a method which expects a SubFoo either. The problem is, the compiler doesn’t really know what ? is. It has to play it safe because it doesn’t know whether ? is Foo or SubFoo, and it doesn’t know how the method we’re calling will use that object.

Eric Burke Says:

@Bob…that “new Foo() {}” was a mistake on my part. That was some leftover cruft from some experimentation I was doing. Thanks for pointing out that <T extends E> boolean contains(T o); is equivalent…that didn’t really occur to me, but seems obvious now that you state it.

Doug Says:

I’m not sure that I follow what the whole uproar is about. From what I can tell, Kevin Bourrillion got off on a wrong tangent by incorrectly assuming substitutability where there is none. A Set of SubFoo cannot be substituted for a Set of Foo. If it could, then some wiseacre could add an object of type OtherSubFoo to the set.

Arrays are a special case in Java (see JLS3 4.10.3), and you can indeed substitute an array of SubFoo for an array of Foo. This is because the array element type is available to be dynamically verified at runtime. Due to type erasure, no such runtime verification is possible with Java generics.

In Scala, one can designate a type parameter as being covariant to allow this; the Scala compiler will then flag as errors any code that is not compatible with that type being covariant. Java doesn’t have this feature.

Eric Burke Says:

“uproar” ???

Daniel Yokomizo Says:

The problem is that your doSomethingReading method is saying to the compiler, I require a MySet of any generic type that is a subtype of Foo. So T can be Foo or SubFoo. If T is SubFoo, it’s obvious to see that it won’t work (i.e. Foo can’t be assigned to SubFoo without casting). To your problem compile you need the super wildcard: boolean contains(T o). With this in place MySet is saying that it can contain Foo or any supertype of i. Not much useful though.

Leave a Reply