为什么 StreamEx 强制我在收集到列表时将“?扩展”添加到变量类型?

Why StreamEx force me to add "? extends" to variable type when collecting to list?

我注意到在低于标准 Java 的情况下,流比 StreamEx 更好地“计算”变量类型。 这很奇怪,因为人们喜欢 StreamEx 并在任何地方都使用它,但是代码却被“?”污染了。 我想使用 List<Class<?>,但 StreamEx 强制我使用 List<? extends Class<?>>。有人可以解释为什么 StreamEx 以这种方式工作吗?我能以某种方式将所需的变量类型与 StreamEx 一起使用吗?

private static List<? extends Class<?>> 
fooList_StreamEx_Compiles(List<Integer> input) {
    return StreamEx.of(input)
            .map(x -> foo())
            .toList();
}

private static List<Class<?>> 
fooList_StreamEx_Error(List<Integer> input) {
    return StreamEx.of(input)
            .map(x -> foo())
// Error: incompatible types: java.util.List<java.lang.Class<capture#1 of ?>> 
// cannot be converted to java.util.List<java.lang.Class<?>>
            .toList();
}

private static List<Class<?>> fooList(List<Integer> input) {
    return input
            .stream()
            .map(x -> foo())
            .collect(Collectors.toList());
}

private static Class<?> foo() {
    return String.class;
}

我正在使用 StreamEx 0.7.0 和 Java 11

这不是 StreamEx 问题。当您在 StreamEx 上使用 collect(Collectors.toList()) 时,它同样有效。问题与 toList() 便利方法有关,标准 Stream 甚至不提供。 StreamEx是个普遍问题,不怪

StreamEx 上的 toList 方法具有以下签名:

public List<T> toList()

在理想情况下,创建一个用 super-type 参数化的列表是合法的,即

public <R super T> List<R> toList()

但在创建 Java 的泛型时,此语法已被省略。 CollectionStreamtoArray 方法都有类似的限制;它们不能将结果数组的元素类型声明为集合元素类型的超类型。但是由于检查了实际数组的存储操作,这些方法只允许任何元素类型。对于 List 结果,要进行类型擦除,这是不可能的。

另一方面,当使用 collect(Collectors.toList()) 时,由于 collect 的签名,可以创建具有超类型的列表:

<R,​A> R collect​(Collector<? super T,​A,​R> collector)

它允许传入一个 Collector 参数化的超类型 (? super T),在大多数情况下将从目标类型推断出来。


toList 声明的这一限制与另一个限制相互作用,即对通配符类型的不合理处理。我不确定这个问题的原因是编译器还是规范,但在 map(x -> foo()) 步骤,捕获了通配符类型,并且这种捕获的类型将被认为不同于任何其他捕获的通配符类型,即使当它来自同一来源时。

当我用 javac 编译你的代码时,它说:

error: incompatible types: List<Class<CAP#1>> cannot be converted to List<Class<?>>
                    .toList();
                           ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error

CAP#1 是捕获类型。所有捕获的类型都会编号,以区分它们。如前所述,它们中的每一个都被认为是一种独特的类型,彼此不同。

Class<?>Class<CAP#1> 的超类型,因此它与 collect 一起使用,它允许收集到使用该超类型参数化的列表,如上所述,但不适用于 toList.

您可以通过使用 return 类型 List<? extends Class<?>> 来解决此问题,以表示列表的实际元素类型是 Class<?> 的子类型,但更好的选择是强制编译器不使用捕获的类型:

private static List<Class<?>> fooList_StreamEx_Solved(List<Integer> input) {
    return StreamEx.of(input)
            .<Class<?>>map(x -> foo())
            .toList();
}

如果您不完全理解在这里插入显式类型的必要性,请不要担心,我并不是说 Java 编译器的行为在涉及通配符类型时是完全可以理解的……