ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

I
InverseFalcon

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


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."
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...
That notation "Collections.emptyList()" is really strange, but makes sense. Easier than Enum>. :)
Supplying a type parameter is not required in Java 8 anymore (unless there is an ambiguity in possible generic types).
The second snippet does show type inference nicely but won't compile, of course. The call to this must be first statement in the constructor.
c
carson

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;

flawless answer @carson
D
Dan Vinton

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.


L
Lii

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!