Collections.emptyList() returns a List<Object>?

JavaGenericsType Inference

Java Problem Overview


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()?

Java Solutions


Solution 1 - Java

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>.

Solution 2 - Java

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;

Solution 3 - Java

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.

Solution 4 - Java

Since Java 8 this kind of code compiles as expected and the type parameter gets inferred by the compiler.

public Person(String name) {
    this(name, Collections.emptyList()); // Inferred to List<String> in Java 8
}

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

The new thing in Java 8 is that the target type of an expression will be used to infer type parameters of its sub-expressions. Before Java 8 only direct assignments and arguments to methods where used for type parameter inference.

In this case the parameter type of the constructor will be the target type for Collections.emptyList(), and the return value type will get chosen to match the parameter type.

This mechanism was added in Java 8 mainly to be able to compile lambda expressions, but it improves type inferences generally.

Java is getting closer to proper Hindley–Milner type inference with every release!

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionChris ConwayView Question on Stackoverflow
Solution 1 - JavaInverseFalconView Answer on Stackoverflow
Solution 2 - JavacarsonView Answer on Stackoverflow
Solution 3 - JavaDan VintonView Answer on Stackoverflow
Solution 4 - JavaLiiView Answer on Stackoverflow