为什么使用泛型的Collections.emptySet()在赋值时可以正常工作,但作为方法参数却不能?

59

所以,我有一个类,其中构造函数如下:

public FilterList(Set<Integer> labels) {
    ...
}

我想构建一个新的FilterList对象并使用空集合。根据Joshua Bloch在他的书《Effective Java》中的建议,我不想为空集合创建新的对象;我将使用Collections.emptySet()代替:

FilterList emptyList = new FilterList(Collections.emptySet());

这让我报错,抱怨 java.util.Set<java.lang.Object> 不是一个 java.util.Set<java.lang.Integer>。好的,那这个呢:

FilterList emptyList = new FilterList((Set<Integer>)Collections.emptySet());

这个也给我出错了!好的,那么这个呢:

Set<Integer> empty = Collections.emptySet();
FilterList emptyList = new FilterList(empty);

嘿,它起作用了!但是为什么?毕竟Java没有类型推断,这就是为什么如果你做Set<Integer> foo = new TreeSet()而不是Set<Integer> foo = new TreeSet<Integer>(),你会得到未经检查的转换警告。但是Set<Integer> empty = Collections.emptySet();却可以在没有警告的情况下工作。为什么呢?


2
以下所有答案都是正确的,但我不明白的是:为什么你要用emptyList来初始化你的集合,而不是调用一个没有参数的默认构造函数?我所知道的每个集合都有一个带有现有集合的构造函数和一个空构造函数。 - Sean Patrick Floyd
有趣的是,以下代码确实可以编译通过:FilterList emptyList = new FilterList((Set<Integer>)(Set<? extends Object>)Collections.emptySet()); - EricS
1
另一个相当有趣的例子:Set<Integer> emptySet = (Set<Integer>)Collections.emptySet(); 无法编译。 - neo
4个回答

128
简短的回答是 - 这是 Java 泛型系统类型推断的限制。它可以根据具体变量推断泛型类型,但无法根据方法参数进行推断。
我怀疑这是因为方法根据拥有对象的运行时类动态分派,因此在编译时(解析所有泛型信息的时间)你不能确定方法参数的类别,也就不能做出推断。变量声明是良好且不变的,所以你可以这样做。
无论如何,你始终可以像这样显式指定泛型调用的类型参数:
Collections.<Integer>emptySet();

或者一次传递多个参数,例如:

Collections.<String, Boolean>emptyMap(); // Returns a Map<String, Boolean>

在推断无法发挥作用的情况下,这往往比需要进行转换看起来更加简洁。


2
如果解释得很好,给予+1。我希望在SO中可以标记两个正确答案 :) (如果要求繁体中文,可以在问题中注明) - jdmichal
3
我知道编译器在推断时不会考虑方法调用的上下文,但我不确定原因。至少对于私有或“final”方法,进行推断应该是可能的。 - Hank Gay
2
@Hank:或者static方法,在编译时也会被解析。但是 - 它并不会,我想这就是主要的观点。 - Andrzej Doyle
我怀疑这是因为方法根据所属对象的运行时类动态分派,因此在编译时...你不能确定方法参数的类别,因此无法推断。那么为什么带类型转换的版本不起作用呢? - EricS
由于方法重载,像 new FilterList(Collection.emptySet()) 这样的表达式无法推断出类型,因为你可以有 FilterList(Set<Integer>)FilterList(Set<String>),所以编译器不知道选择哪一个。 - neo
1
我怀疑这是因为方法的调度取决于拥有对象的运行时类。我不确定是否是同一件事,但要调用重载方法的哪个版本是在编译时确定的:https://ampersand.space/blog/2007/1/12/java-overload/。 - Kip

7

尝试

FilterList emptyList = new FilterList(Collections.<Integer>emptySet());

对于具有类型参数的方法,您可以强制指定类型参数,以防推断不够精确,或者允许您使用子类型。例如:

// forces use of ArrayList as parameter instead of the infered List
List<String> l = someObject.<ArrayList<String> methodThatTakesTypeParamForReturnType();

6
您想要做到这一点:
FilterList emptyList = new FilterList(java.util.Collections.<Integer>emptySet());

这告诉emptySet方法,其泛型参数应明确为Integer,而不是默认的Object。是的,对于此来说,语法完全奇怪且非直观。 :)


1
我之前不知道那个语法,谢谢!但我仍然想知道为什么在变量赋值时不需要那个语法。 - Karl von L
请参考Andrzej Doyle的回答,我认为那是一个很好的解释。 - jdmichal
你不需要在那里使用 new。事实上,我认为如果使用它,代码将无法编译。 - Andrei Fierbinteanu

5

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