为什么Java编译器不能< String>从约束“Iterable〈?extends CharSequence>and()->(Iterator< String>)"推断出Iterable?

whhtz7ly  于 2023-02-28  发布在  Java
关注(0)|答案(2)|浏览(108)

背景:我最近写了一个答案,我建议写下面的代码:

Files.write(Paths.get("PostgradStudent.csv"),
        Arrays.stream(PGstudentArray).map(Object::toString).collect(Collectors.toList()),
        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

经过一番思考,我决定:“我实际上不需要列表,我只需要一个Iterable<? extends CharSequence>“由于Stream<T>有一个方法Iterator<T> iterator(),我想,好吧,这很简单:

Iterable<? extends CharSequence> iterable = () -> Arrays.stream(arr).map(Object::toString).iterator();

(我把它提取到一个局部变量中来回答这个问题,我想在最后内联它。)不幸的是,如果没有额外的类型提示,它就不能编译:

error: incompatible types: bad return type in lambda expression
Iterable<? extends CharSequence> iterable = () -> Arrays.stream(arr).map(Object::toString).iterator();
                                                                                                   ^
    Iterator<String> cannot be converted to Iterator<CharSequence>

当然,添加一些类型提示也可以做到:

Iterable<? extends CharSequence> iterable2 = (Iterable<String>) () -> Arrays.stream(arr).map(Object::toString).iterator();
Iterable<? extends CharSequence> iterable3 = () -> Arrays.stream(arr).<CharSequence>map(Object::toString).iterator();

在我的理解中,Java编译器执行以下操作:
1.它查看表达式的目标类型,即Iterable<? extends CharSequence>
1.然后它确定这个接口的函数类型,在我的例子中是() -> Iterator<? extends CharSequence>
1.然后,它查看lambda并检查它是否兼容,在我的例子中,lambda的类型为() -> Iterator<String>,它与步骤2中确定的函数类型兼容。
有趣的是,如果我将lambda的目标改为Supplier

Supplier<Iterator<? extends CharSequence>> supplier = () -> Arrays.stream(arr)
    .map(Object::toString)
    .iterator();

它将编译良好。
为什么javac不能为这个lambda推断出正确的类型?

cdmah0mi

cdmah0mi1#

你可以找到一些解释here
通配符参数化函数接口类型必须在检查兼容性之前转换为函数类型(方法签名)...其工作原理如下:

Iterable<? extends CharSequence> becomes () -> Iterator<CharSequence>

因此,如果lambda表达式是隐式类型的,则LHS变为Iterator<CharSequence>,而RHS为Iterator<String>

Iterator<String> cannot be converted to Iterator<CharSequence>

JLS §18.5.3中也解释了这种行为。

hc2pp10m

hc2pp10m2#

在阅读了另一个答案(这是绝对正确的)和一些咖啡之后,看起来bug中的解释相当合乎逻辑。
这里有两种情况:一个 * 显式 * lambda类型和一个 * 隐式 * lambda类型。显式类型为:

Iterable<String> one = () -> Arrays.stream(arr).map(Object::toString).iterator();
Iterable<? extends CharSequence> iterable = one;

或者像OP的例子一样:

Iterable<? extends CharSequence> iterable2 = (Iterable<String>) () -> Arrays.stream(arr).map(Object::toString).iterator();

我们直接告诉编译器lambda表达式是哪种类型:Iterable<String>.
在这种情况下,编译器只需要做一件事:查看目标是否可分配给该类型;很容易找到,而且与lambdas本身没有太大关系。
另一种类型是 * implicit * 类型,当编译器必须 * 推断 * 类型时,事情变得有点棘手。"棘手"的部分来自于目标使用通配符的事实,因此可能匹配不止一个选项。可能有无数种方法(当然是有限的,但只是为了证明一点)可以将lambda推断为。
它可以这样开始,例如:

Iterator<? extends Serializable> iter = Arrays.stream(arr).map(Object::toString).iterator();

无论进一步做什么,这都将失败:CharSequence不能扩展Serializable,但String可以;我们将不能将Iterable<? extends CharSequence> iterable分配给"具有可串行化的任何推断类型"。
或者可以这样开头:

Iterator<? extends Comparable<? extends CharSequence>> iter = Arrays.stream(arr).map(Object::toString).iterator();

因此,理论上,编译器可以开始推断该类型可能是什么,并逐个检查"某个"推断的类型是否可以匹配目标;但显然需要大量的工作因此不进行。
另一种方法要简单得多,"切割"目标,从而将推理的可能性降到只有一个,一旦目标被转换为:

Iterable<CharSequence> iterable...

编译器必须做的工作要简单得多。
顺便说一句,这不是我第一次在lambdas中看到这种隐式与显式类型的逻辑。

相关问题