Collections.emptyList()返回一个List<Object>吗?

286

我在使用Java的泛型类型参数推断方面遇到了一些困难。考虑下面这个类,它有一个可选的列表参数:

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;
  }
}

我的Java编译器报错如下:

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

但是 Collections.emptyList() 返回的类型为 <T> List<T>,而不是 List<Object>。强制类型转换并不能解决问题。

public Person(String name) {
  this(name,(List<String>)Collections.emptyList());
}
产生的结果
Person.java:9: inconvertible types

使用EMPTY_LIST代替emptyList()

public Person(String name) {
  this(name, Collections.EMPTY_LIST);
}
产出。
Person.java:9: warning: [unchecked] unchecked conversion

而以下更改使错误消失:

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

有人能够解释我这里遇到的类型检查规则是什么,以及最好的解决方法是什么吗?在这个例子中,最后的代码示例是令人满意的,但对于更大的类,我想能够编写遵循这种“可选参数”模式的方法,而不需要重复代码。

额外加分:何时适合使用EMPTY_LIST而不是emptyList()


2
对于所有与Java泛型相关的问题,我强烈推荐Maurice Naftalin和Philip Wadler所著的《Java Generics and Collections》(http://oreilly.com/catalog/9780596527754/)。 - Julien Chastang
4个回答

477

您遇到的问题是尽管方法emptyList()返回List<T>,但您没有为其提供类型,因此它默认返回List<Object>。 您可以提供类型参数,并使代码按预期运行,如下所示:

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

现在,当您进行简单的赋值操作时,编译器可以为您找出泛型类型参数。这被称为类型推断。例如,如果您执行以下操作:

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

那么调用 emptyList() 将会正确返回一个 List<String>


14
明白了。作为一名来自机器学习领域的人,我觉得有些奇怪,Java不能推断正确的类型:形式参数的类型和emptyList的返回类型显然是可以统一的。但我想类型推断器可能只能迈出“婴儿步”。 - Chris Conway
5
在某些简单情况下,编译器似乎可以推断出这种情况下缺失的类型参数,但这可能是危险的。如果存在多个具有不同参数的方法版本,则可能会调用错误的方法版本。而且第二个方法版本甚至可能尚不存在... - Bill Michell
13
那个符号“Collections.<String>emptyList()”看起来很奇怪,但是很有道理。比“Enum<E extends Enum<E>>”容易理解。 :) - Thiago Chaves
12
在Java 8中,除非存在可能的泛型类型歧义,否则不再需要提供类型参数。 - Vitalii Fedorenko
10
第二个代码片段展示了很好的类型推断,但是它当然不会编译通过。调用this必须是构造函数中的第一条语句。 - Arjan
2
@ChrisConway 对于聪明的类型推断器,特别是那些跨越方法或模块边界进行推理的推论,存在一个“软件工程”争论。类型是接口的一部分,如果您不明确地写下它,它可能会随着时间的推移而不可预测地漂移。您无法编写推断出的类型(它们往往对人类不友好,有时甚至无法用语言表达,它们在“所有这些仍然有效”的意义上是“最大化的”,而不是以人为中心的“这就是我们正在构建的内容,我们将用它进行测试,这是我们关心的内容”。) - user7610

108

您想使用:

Collections.<String>emptyList();

如果您查看emptyList的源代码,您会发现它实际上只是执行了一个

return (List<T>)EMPTY_LIST;

无瑕的答案 @carson - Gaurav

27

emptyList 方法的签名如下:

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

<T>在单词List之前的意思是从结果分配给的变量类型推断出通用参数T的值。所以在这种情况下:

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

返回值被显式地引用为类型为List<String>的变量,以便编译器可以算出它。在这种情况下:

setList(Collections.emptyList());

编译器没有明确的返回变量来确定泛型类型,因此默认为 Object


1

自Java 8以来,这种代码能够如预期编译,并且类型参数将被编译器推断。

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;
}

Java 8中的新特性是使用表达式的目标类型来推断其子表达式的类型参数。在Java 8之前,只有直接赋值和方法参数用于类型参数推断。
在这种情况下,构造函数的参数类型将成为Collections.emptyList()的目标类型,并且返回值类型将被选择以匹配参数类型。
这种机制主要是为了编译lambda表达式而添加到Java 8中的,但它通常会改进类型推断。
随着每个版本的发布,Java越来越接近适当的Hindley-Milner类型推断!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接