Java中奇怪的显式类型参数声明语法的用途

14

我最近发现了在调用Java方法时显式声明泛型类型的奇怪语法。例如:

Collections.<String>emptyList();

返回一个空的 List<String>。然而,这似乎有些愚蠢,因为<T> emptyList() 的实现只是未经检查的类型转换 (List<T>) EMPTY_LIST,因此所有结果都具有相同的类型擦除(并且是同一个对象)。此外,通常不需要这种明确的类型声明,因为编译器通常可以推断出类型:

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

在进一步调查后,我发现还有两种情况需要使用这种语法,都是由于使用Guava库并且明显尝试将太多语句放在一行上导致的。
  1. Decorating a collection, for example with a synchronized wrapper, and the compiler being not able to infer the types. The following doesn't work if you take out the type declaration: cannot convert from Set<Object> to Set<String>:

    Set<String> set = Collections.synchronizedSet(Sets.<String>newHashSet());
    
  2. Getting less specific type parameters when they compiler tries to make ones that are too specific. For example, without the type declaration the following statement complains as well: cannot convert from Map<String, String> to Map<String, Object>:

    Map<String, Object> toJson = ImmutableMap.<String, Object>of("foo", "bar");
    

我觉得有些讽刺的是,在第一个情况中,推断出的类型参数过于泛化,而在第二种情况中则过于具体,但我想这只是Java中泛型系统的一种产物。

然而,这种语言结构本身似乎是可以避免的,除了在Guava团队发明的奇怪用例之外。此外,对我来说,编译器在上述两个示例中都可以推断出类型参数,开发人员只是选择不这样做。在Java编程中是否存在必须或有用使用这种结构的示例,还是它仅存在于使编译器更简单/ JDK开发人员更轻松的目的?


+1 我很幸运之前从未见过这种表示法。 - Armand
2
请注意,Guava的作者并没有“发明”这些用例 - 他们只是因为该库的语义特别暴露而大量应对了它们。 - Paul Bellora
1
如果您在此处未使用Guava静态方法,则仍需显式指定类型参数,只是语法不同 - 例如Collections.synchronizedSet(new HashSet<String>()) - Louis Wasserman
请注意,在这里给出的所有示例中,不再需要显式类型参数。这是因为Java 8中的类型推断得到了很大的改进。 - Lii
3个回答

5

"关闭编译器"为什么不是"必要或有用的"?我发现它对于我的代码编译来说既是必要的,是有用的。

有时候,正确的类型不能被推断出来,就像你已经发现的那样。在这种情况下,需要显式地指定类型参数。一些编译器无法智能处理的例子如下:

如果你真的想深入了解类型推断的复杂性,就需要从Java语言规范开始,并以此结束。你需要关注JLS §15.12.2.7. 基于实际参数推断类型参数§15.12.2.8. 推断未解决的类型参数


如果你这么说,那么你是在说没有办法向Java语言中添加泛型,以便不必存在这个问题吗?看起来编译器可以被设计成推断出我提供的所有示例中的参数,但是实现它的人选择不这样做。我稍微编辑了我的问题,所以你可能需要更新你的答案。 - Andrew Mao
2
Java泛型存在许多不足之处,包括但不限于不完美的类型推断、类型擦除、缺乏集合协变等。在某些世界中,当然,在您的Guava示例中可能会出现类型推断,但我们并不生活在那个世界中。Guava是为了与这个世界的Java编译器和有缺陷的泛型类型系统一起工作而编写的,因此我们只能使用我们所拥有的东西。 - Matt Ball
如果你在想,Java泛型类型系统的许多缺陷都源于最初决定以向后兼容的方式实现泛型。 - Matt Ball

1
我发现至少有一种情况,编译器能够正确地推断类型,并且仍然需要:当您想将结果用作更通用的类型时。考虑这个方法,它基本上从零个或多个T对象创建一个List<T>:
public static <T> List<T> listOf(T... items) {
    ArrayList<T> list = new ArrayList<T>();
    for (T item : items)
        list.add(item);

    return list;
}

这句话的意思是:“你可以像这样使用它:”。同时保留了HTML格式,不做解释。
List<Integer> numbers = ListUtils.listOf(1, 2, 3);

现在,假设您有一个可以接收 List<Object> 的方法:

public static void a(List<Object> objs) {
    ...
}

假设你想提供一个通过 listOf() 方法构建的列表:

a(ListUtils.listOf(1, 2, 3));

这段代码无法编译,因为方法参数类型是List<Object>,而提供的参数是List<Integer>。在这种情况下,我们可以将调用更改为:

a(ListUtils.<Object>listOf(1, 2, 3));

它按预期编译。


在给定的示例中,更好的解决方案可能是将方法参数设置为List<? extends Object>,这将接受一个List<Integer>。但是,这会禁止该方法向列表中添加除null以外的任何内容。 - Sean Van Gorder

0
Java类型推断非常弱。只有在方法的结果定义变量时才不需要在通用方法(如emptyList())中包含显式类型。如果你尝试将空列表作为另一个方法的参数传递(示例1),这种情况对我来说每天都会出现(而我还没有使用Guava),编译器就会完全放弃类型推断。我看不出将空列表声明为本地单次使用变量是如何“将太多语句放在一行”中,正如你所说;空列表是一个非常简单的子表达式,除了Java可悲的类型推断使其复杂。与Scala相比,后者会在3种不同的情况下进行推断。

我不理解为什么“方法的结果定义了一个变量”不能延伸到像synchronizedSet()这样的情况,其中类型<T>直接通过方法传递,因此应该能够推断出传入的任何内容应该是什么类型。 - Andrew Mao
我同意,这很糟糕。但那就是语言规范。空集合调用是一个参数,但推断只允许在顶层进行。这种类型的示例在我的代码中非常普遍,不幸的是,因为其他选择甚至更糟。 - Judge Mental

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