views:

7038

answers:

4

I'm having some trouble navigating Java's rule for inferring generic type parameters. Consider the following class, which has an optional list parameter:

import java.util.Collections;
import java.util.List;

public class Person {
  private String name;
  private List<String> nicknames;

  public Person(String name) {
    this(name,Collections.emptyList());
  }

  public Person(String name,List<String> nicknames) {
    this.name = name;
    this.nicknames = nicknames;
  }
}

My Java compiler gives the following error:

Person.java:9: The constructor Person(String, List<Object>) is undefined

But Collections.emptyList() returns type <T> List<T>, not List<Object>. Adding a cast doesn't help

public Person(String name) {
  this(name,(List<String>)Collections.emptyList());
}

yields

Person.java:9: inconvertible types

Using EMPTY_LIST instead of emptyList()

public Person(String name) {
  this(name,Collections.EMPTY_LIST);
}

yields

Person.java:9: warning: [unchecked] unchecked conversion

Whereas the following change makes the error go away:

public Person(String name) {
  this.name = name;
  this.nicknames = Collections.emptyList();
}

Can anyone explain what type-checking rule I'm running up against here, and the best way to work around it? In this example, the final code example is satisfactory, but with larger classes, I'd like to be able to write methods following this "optional parameter" pattern without duplicating code.

For extra credit: when is it appropriate to use EMPTY_LIST as opposed to emptyList()?

+11  A: 

You want to use:

Collections.<String>emptyList();

If you look at the source for what emptyList does you see that it actually just does a

return (List<T>)EMPTY_LIST;
carson
nicknames is a List<String> not List<Person>
Stephen Denne
Woops, I was looking at the cast he tried. Thanks for pointing it out.
carson
+6  A: 

the emptyList method has this signature:

public static final <T> List<T> emptyList()

That <T> before the word List means that it infers the value of the generic parameter T from the type of variable the result is assigned to. So in this case:

List<String> stringList = Collections.emptyList();

The return value is then referenced explicitly by a variable of type List<String>, so the compiler can figure it out. In this case:

setList(Collections.emptyList());

There's no explicit return variable for the compiler to use to figure out the generic type, so it defaults to Object.

Dan Vinton
+29  A: 

The issue you're encountering is that even though the method emptyList() returns List<T>, you haven't provided it with the type, so it defaults to returning List<Object>. You can supply the type parameter, and have your code behave as expected, like this:

public Person(String name) {
  this(name,Collections.<String>emptyList());
}

Now when you're doing straight assignment, the compiler can figure out the generic type parameters for you. It's called type inference. For example, if you did this:

public Person(String name) {
  List<String> emptyList = Collections.emptyList();
  this(name, emptyList);
}

then the emptyList() call would correctly return a List<String>.

InverseFalcon
Got it. Coming from the ML world, it's weird to me that Java can't infer the correct type: the type of formal parameter and the return type of emptyList are clearly unifiable. But I guess the type inferencer can only take "baby steps."
Chris Conway
In some simple cases, it might seem possible for the compiler to infer the missing type parameter in this case - but this could be dangerous. If multiple versions of the method existed with different parameters, you might end up calling the wrong one. And the second one might not even exist yet...
Bill Michell
That notation "Collections.<String>emptyList()" is really strange, but makes sense. Easier than Enum<E extends Enum<E>>. :)
Morgaelyn
+2  A: 

For all Java Generics related questions, I highly recommend "Java Generics and Collections" by Maurice Naftalin, Philip Wadler.

Julien Chastang