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

huangapple go评论58阅读模式
英文:

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())
// 错误:不兼容的类型:java.util.List<java.lang.Class<capture#1 of ?>> 
// 无法转换为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。

英文:

I noticed that in the case below standard Java stream better "calculate" variable type than StreamEx.
This is odd, because people love StreamEx and use it everywhere, but then code become polluted with "?".
I want to use List&lt;Class&lt;?&gt; but StreamEx force me to use List&lt;? extends Class&lt;?&gt;&gt;. Could someone explain why StreamEx works this way? Can I somehow use the desired variable type with StreamEx ?

<!-- language: java -->

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

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

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

private static Class&lt;?&gt; foo() {
    return String.class;
}

I'm using StreamEx 0.7.0 and Java 11

答案1

得分: 4

这并不是一个 StreamEx 的问题。当你在 StreamEx 上使用 collect(Collectors.toList()) 时,它同样有效。问题与 toList() 这个便利方法有关,标准的 Stream 甚至没有提供这个方法。这是一个一般性的问题,不应归咎于 StreamEx

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

public List<T> toList()

在理想的情况下,参数化超类型的列表是合法的,即

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

但是,当 Java 的泛型被创建时,这种语法被省略了。CollectionStreamtoArray 方法都受到类似限制的影响;它们不能声明生成的数组元素类型为集合元素类型的超类型。但由于实际数组的存储操作是受检查的,这些方法只是允许任何元素类型。对于 List 结果,由于类型擦除,这是不可能的。

然而,当使用 collect(Collectors.toList()) 时,由于 collect 的签名,可以使用带有超类型参数化的 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

你可以通过使用返回类型 List<? extends Class<?>> 来修复这个问题,以表示列表的实际元素类型是 Class<?> 的子类型,但更好的选择是强制编译器不使用捕获的类型:

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

如果你并没有完全理解在这里插入显式类型的必要性,不用担心,我并不是在声称当涉及通配符类型时 Java 编译器的行为是可以完全理解的...

英文:

This is not a StreamEx issue. When you use collect(Collectors.toList()) on the StreamEx, it works equally well. The problem is connected with the toList() convenience method, which the standard Stream doesn’t even offer. It’s a general problem that StreamEx is not to blame for.

The toList method on StreamEx has the following signature:

public List&lt;T&gt; toList()

In a perfect world, it would be legal to create a list parameterized with a super-type, i.e.

public &lt;R super T&gt; List&lt;R&gt; toList()

but this syntax had been omitted when Java’s generics were created. The toArray methods of both, Collection and Stream suffer from a similar limitation; they can not declare the resulting array’s element type to be a super type of the collection’s element type. But since an actual array’s store operation is checked, those methods simply allow any element type. For a List result, subject to type erasure, this is not possible.

When using collect(Collectors.toList()) on the other hand, it’s possible to create a list with a super type due to collect’s signature:

&lt;R,A&gt; R collect(Collector&lt;? super T,A,R&gt; collector)

which allows to pass in a Collector parameterized with a super type (? super T) that in most scenarios will be inferred from the target type.


This limitation of the toList declaration interacts with another limitation, the unreasonable handling of wildcard types. I’m not sure whether the cause of this problem lies in the compiler or specification, but at the map(x -&gt; foo()) step, the wildcard type got captured and such a captured type will be considered different to any other captured wildcard type, even when it stems from the same source.

When I compile your code with javac, it says:

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

The CAP#1 is a captured type. All captured types get numbered, to distinguish them. As said, each of them is considered a distinct type, different from every other.

Class&lt;?&gt; is a supertype of Class&lt;CAP#1&gt;, so it works with collect which allows to collect to a list parameterized with that supertype, as said above, but not with toList.

You can fix this by using the return type List&lt;? extends Class&lt;?&gt;&gt;, to denote that the list’s actual element type is a subtype of Class&lt;?&gt;, but the better option is to force the compiler not to use a captured type:

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

Don’t worry if you didn’t fully understand the necessity to insert the explicit type here, I’m not claiming that the behavior of Java compilers was comprehensible at all when wildcard types are involved…

huangapple
  • 本文由 发表于 2020年9月18日 04:50:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/63946023.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定