01 July 2009

Type Inference in Java, for Generics

An unexpected failure of the Java compiler. Or is it a failure of the Java type system?

Java programmers write repetitive code like

HashMap<String, Integer> foo = new HashMap<String, Integer>();

all the time. Those who seek beauty in their code may choose to use the Google collections library and write instead

HashMap<String, Integer> foo = Maps.newHashMap();

Constructors and static methods interact in very different ways with generics. If you say new Foo() you instantiate a raw type, which is quite different from a what you get by saying new Foo<Bar>(). On the other hand, static methods are the beneficiaries of a little bit of type inference, as specified by sections 15.12.2.7 and 15.12.2.8 of the Java Language Specification (Third Edition). The actual rules are complicated (and I haven't read them yet) but the idea seems to be to use two sources of information:

  • the types of the actual arguments
  • if the result is "assignment convertible" to some type T then this type T is used too.

The reason why the Google collections library works is the second bullet: The compiler uses the type of foo to figure out how to instantiate the type arguments of Maps.newHashMap.

OK, that was the background. Now let's see some examples.

Would you expect the following code to compile?

interface A {
  static enum Foo { BAR; }
  static enum Bar { FOO; }
}

class B<T extends Enum<T>> {
  private B() {}
  public static <T extends Enum<T>> B<T> get() {
    return new B<T>();
  }
}

public class C {
  public static void main(String[] args) {
    B<A.Foo> b = B.get();
  }
}

Yes, it works. The Sun Java compiler 1.6.0_13 has no problem in handling the sort-of recursive bound put on the type.

Now, would you expect the following to work?

interface A {
  static enum Foo { BAR; }
  static enum Bar { FOO; }
}

class B<T, S> {
  private B() {}
  public static <T, S> B<T, S> get() {
    return new B<T, S>();
  }
}

public class C {
  public static void main(String[] args) {
    B<A.Foo, A.Bar> b = B.get();
  }
}

Again, it works flawlessly. After all, why would multiple type arguments confuse the compiler?

So, you'd expect the following to work too, won't you?

interface A {
  static enum Foo { BAR; }
  static enum Bar { FOO; }
}

class B<T extends Enum<T>, S extends Enum<S>> {
  private B() {}
  public static <T extends Enum<T>, S extends Enum<S>> B<T, S> get() {
    return new B<T, S>();
  }
}

public class C {
  public static void main(String[] args) {
    B<A.Foo, A.Bar> b = B.get();
  }
}

Tough luck, though: It doesn't work. Apparently "no instance(s) of type variable(s) T,S exist so that B<T,S> conforms to B<A.Foo,A.Bar>". I would have thought that T=A.Foo and S=A.Bar would do the job.

As I said, I haven't read yet the relevant sections in the Java specification to see if this a problem with the language or a problem with the compiler. The thing is that the two sections take 10 and a half screens on my monitor, which signals that I should put aside maybe one whole day for it, and I don't have one whole day to spend. My hope is that it is a bug in the compiler, because otherwise I'd loose some confidence in the language itself. After all, there's something fishy about a 10-pages algorithm that doesn't end up trying the obvious solution.

5 comments:

Anonymous said...

This code works for 1.6.0_14 on my Vista machine.

rgrig said...

Thank you for trying. I get the error with 1.6.0_14 on Ubuntu.

Anonymous said...

I would expect, you use

public static <Q extends Enum<Q>> B<Q> get()
instead of
public static <T extends Enum<T>> B<T> get()

rgrig said...

FWIW, this still doesn't compile on my computer, this time with 1.7.0_13 on Ubuntu.

rgrig said...

It does compile now, with 1.7.0_55.

Post a Comment

Note: (1) You need to have third-party cookies enabled in order to comment on Blogger. (2) Better to copy your comment before hitting publish/preview. Blogger sometimes eats comments on the first try, but the second works. Crazy Blogger.