我遇到了以下代码:
public static <T> Set<T> distinct(
Collection<? extends T> list,
Comparator<? super T> comparator) {
Set<T> set = new TreeSet<>(comparator);
set.addAll(list);
return set;
}
此代码仅使用中间TreeSet
来删除重复项,其中元素之间的相等性是根据提供的比较器定义的。
让我们给本地类型推理一个机会,我(天真)想...所以我将上面的代码更改为:
public static <T> Set<T> distinct(
Collection<? extends T> list,
Comparator<? super T> comparator) {
var set = new TreeSet<>(comparator);
set.addAll(list);
return set;
}
这对我来说很有意义,因为set
的类型可以从comparator
的类型推断,或者我认为是这样。但是,修改后的代码不会编译并生成以下错误:
java: incompatible types: java.util.TreeSet<capture#1 of ? super T> cannot be converted to java.util.Set<T>
现在,我理解为什么会出现错误,并且我承认比较器的类型实际上是Comparator<? super T>
,因此var
推断出的类型是TreeSet<? super T>
。
但是,我想知道为什么var
无法将TreeSet
的泛型类型推断为T
而不是? super T
。毕竟,according to the docs,TreeSet<E>
有一个接受Comparator<? super E>
类型参数的构造函数。因此,调用此构造函数应创建TreeSet<E>
,而不是TreeSet<? super E>
。 (这是第一个片段显示的内容)。我希望var
遵循同样的逻辑。
注1:编译代码的一种方法是将返回类型更改为Set<? super T>
。但是,这将是一个难以使用的设置......
注2:另一种方法是不在比较器中使用逆变,但我不想这样,因为我不能使用{{1比较Comparator
的祖先。
注3:我知道第一个代码段有效,所以我应该坚持不使用T
并明确将该集声明为var
。但是,我的问题不是我是否应该丢弃我的第二个片段或如何解决它。相反,我想知道为什么Set<T>
没有推断var
作为我的第二个代码段中TreeSet<T>
局部变量的类型。
编辑1:在this comment中,用户@nullpointer正确地指出我应该进行以下细微更改以使第二个代码段编译:
set
现在,var set = new TreeSet<T>(comparator); // T brings in the magic!
明确了泛型类型参数T
,因此TreeSet
正确地将var
局部变量的类型推断为set
。不过,我想知道为什么我必须明确指定TreeSet<T>
。
编辑2:在this other comment中,用户@Holger巧妙地提到以下语言中的禁止:
T
上面的代码无法编译,并出现以下错误:
var set = new TreeSet<? super T>(comparator);
所以现在问题变得更加明显:如果我无法在实例化表达式java: unexpected type
required: class or interface without bounds
found: ? super T
中明确指定有界泛型类型? super T
,为什么编译器会将new TreeSet<? super T>(comparator)
推断为类型TreeSet<? super T>
局部变量?
答案 0 :(得分:8)
根据Brian Goetz' answer的my question,他说:
局部变量类型推断说:我需要的类型可能已经存在于右侧,为什么在左侧重复它们。
关于问题中的代码,唯一可用于推断的类型(使用提供的Comparator
)是TreeSet<? super T>
。我们人类足够聪明,可以看到distinct
返回set
并期望Set<T>
。但是,编译器要么可能不够聪明才能解决它(我确信它可以),但更可能的是var
使用RHS和架构师提供的信息推断出最具体的类型。我不想打破那个。
现在,正如nullpointer在其评论中所述,您可以明确地将TreeSet
定义为类型T
,而不是使用以下内容推断捕获类型? super T
:
var set = new TreeSet<T>(comparator);
我假设显式泛型类型覆盖传递给构造函数的推断类型,这是有意义的。
JLS §14.4.1: Local Variable Declarators and Types似乎支持我的主张,说明如下:
注意:“T的向上投影”,可能只是推断类型(TreeSet
而不是Set
),但也可能包含泛型类型。
我认为这与list
中的var list = List.<Number>of(1, 2, 3);
是List<Number>
而不是List<Integer>
的原因相同,在没有var
的情况下效果也一样。< / p>
答案 1 :(得分:3)
在第二个代码段中使用局部变量要求您明确指定TreeSet
的界限,如下所示:
public static <T> Set<T> distinct(Collection<? extends T> list, Comparator<? super T> comparator) {
var set = new TreeSet<T>(comparator);
set.addAll(list);
return set;
}
原因是推断的var否则会使用与比较器一起使用的最明显的绑定,并被推断为TreeSet<? super T>
,并由于转换的不兼容而失败并显示编译错误。
为什么我必须明确指定T
<击> 按照Jacob所指出的另一种方式思考并将代码表示为
private static Set<Integer> distincts(Collection<? extends Integer> list, Comparator<Number> comparator) {
var set = new TreeSet<>(comparator);
// if you don't specify the bound, you get a compiler error on the return statement
// since the inferred type would be `Number`
set.addAll(list);
return set;
}
要回答问题,默认情况下,您希望在TreeSet
这里推断出哪种类型? Integer
,Byte
来自Number
的子类列表中的哪一个以及如何(基于可能稍后推断的返回类型)?
击>
编辑 : - 我认为给定构造函数 TreeSet(Comparator<? super E> comparator)
构建TreeSet<E>
,因此应该将此类构造函数的调用推断为TreeSet<E>
而不是TreeSet<? super E>
。此外,作为Brian commented,并非所有内容都可以被推断,对于任何此类特定类型,可以要求它(假设在jdk mailing list上)。