将 Java 的 Optional.or() 与子类一起使用时,lambda 表达式中的错误 return 类型

Bad return type in lambda expression when using Java's Optional.or() with subclasses

我正在尝试使用 Optional.or 获取子类 A 的对象,或者如果为空,则获取子类 B 的对象:

interface Node {}
class InnerNode implements Node {}
class LeafNode implements Node {}
Optional<InnerNode> createInnerNode() {}
Optional<LeafNode> createLeafNode() {}

Optional<Node> node = createInnerNode().or(() -> createLeafNode());

执行此操作时,出现以下编译器错误:

Bad return type in lambda expression: Optional<LeafNode> cannot be converted to Optional<? extends InnerNode>

如果我改为使用通配符明确告诉编译器 Optionals 包含一个从 Node:

扩展的对象
Optional<? extends Node> optionalInner = createInnerNode();
Optional<? extends Node> optionalLeaf = createLeafNode();
Optional<Node> node = optionalInner.or(() -> optionalLeaf);

我收到以下编译器错误:

Bad return type in lambda expression: Optional<capture of ? extends Node> cannot be converted to Optional<? extends capture of ? extends Node>

我发现实现此功能的唯一方法是在前面使用 Optional.empty

Optional<Node> node = Optional.<Node>empty() // requires generic argument
    .or(() -> createInnerNode())
    .or(() -> createLeafNode());

但对我来说,这读起来很混乱。是否有可能以某种方式指示编译器允许语句 optionalInner.or(() -> optionalLeaf)?或者还有其他替代方法可以使这项工作有效吗?

这一定是一个令人困惑的错误。让我先解释一下潜在的问题,以便您真正理解 javac 抱怨的原因,然后我将介绍如何解决它。

根本问题是缺少use-site方差

你得到的错误对于 Optional 来说没有意义,但是当我们使用产生 消耗类型化数据的类型时它是完全有意义的(不像 Optional,因为一个实例,只产生数据,例如当你调用 .get() - Optional 上没有 .set() 方法;它是不可变的)。

让我们看看列表。想象一下你可以这样做:

Integer i = 5;
Number n = i; // This is legal java.
List<Integer> ints = new ArrayList<Integer>();
List<Number> numbers = ints; // So this should be too... right?

第 4 行合法。因为泛型默认是不变的(超类型和子类型都不行;在不变类型系统中,如果需要一个数字,那么只有一个数字会做) - 这是正确的。毕竟,如果上面的代码 WAS 合法,那么一切都会崩溃:

Double d = 5.0;
numbers.add(d); // Fine - must be. doubles are numbers!
Integer i = ints.get(0); // um.. wait. uhoh.

因为 numbersints 都只是引用完全相同的列表,如果我向 numbers 添加一个 double,这意味着我也向 ints 添加了一个,和 - 繁荣。坏了。

现在你知道为什么泛型是不变的了。

这也解释了一些进一步的编译器错误:

List<Integer> ints = new ArrayList<Integer>();
List<Number> numbers = ints; // nope, won't compile.
List<? extends Number> numbers = ints; // but this will!
numbers.add(anything-except-literally-null); // this wont!

使用 ? extends 你告诉 java 你 想要 协方差(协方差 = X 的子类型与 X 本身一样好。Java 的 non-generics 类型是协变的)。但是,泛型中的协变自动意味着所有 方法 'send' 将类型数据 禁用。你不能 add 任何东西(除了字面上的 .add(null),因为 null 是所有类型)到 List<?>List<? extends>。如果您考虑一下,这是有道理的:

A List<? extends Number> 可以是 List<Integer>List<Double>。那么如何向这个列表中添加任何内容呢?没有什么是 both a Double and an Integer(好吧,除了文字 null),所以你不可能传递任何东西add 必然是 'safe'。因此,为什么编译器 总是 告诉你这是一个打字违规。

这也意味着这也行不通:

List<? extends Number> numbers = ...;
List<Number> numbers2 = numbers; // nope, this won't compile

返回您的代码

现在,对于可选的,none 这似乎是相关的。 Optional 在其类型参数 中可以完全协变,因为 Optional 不 'consume' 数据 - none 它的方法采用 'E'。但是编译器不是某种能够解决这个问题并让你 'break' typeargs 的变化规则的超级大脑。不是这样 - 可选需要坚持相同的规则列表。因此,如果无法将 List<? extends Number> 的实例分配给 List<Number> 类型的变量,则同样适用于 Optional:

类型 Optional<? extends Node> 的实例不能分配给类型 Optional<Node>

的变量

这就解释了你的错误(对于解决你问题中所述问题的两次尝试)。

即使特别针对可选,这些错误也没有多大意义。现在你知道为什么要得到它们了。

最好的解决方法是不要在这里使用 optional;这并不是它的真正含义,也不是很 java-like(见下文)。但是遥远的第二个最佳选择是修复这些东西。根本的解决办法是所有 接受 可选的地方应该总是被声明为 ? extends。因此,您试图将其分配给类型为 Optional<Node> 的字段,但该字段为 'wrong'。该字段的类型应为 Optional<? extends NOde>。然后解决所有问题:

Optional<? extends Node> node = createInnerNode().or(() -> createLeafNode());

工作正常(这是您的第一个片段,但目标的 'type' 已修复)。

啊。但是,我无法更改 API

嗯,那么API就错了。它发生了 - 有时您需要与损坏的代码进行交互。但别搞错了,那就是破码。您对任何其他损坏的代码执行相同的操作:封装并变通。

你不想与破碎的 API 互动。因此,创建一个 'paper over' 错误的包装器类型或辅助方法。在该代码中('bridging' 解决您无法更改的错误代码的代码),您接受需要编写生成警告的不稳定代码。

您可以'fix things'如下:

public Optional<Node> fixIt(Optional<? extends Node> node) {
  Optional raw = node;
  return raw;
}

之所以可行,是因为泛型最终只是 javac 的想象——JVM 根本不知道泛型是什么。所以我们只需要'fake out'javac停止拒绝编译代码即可。以上将生成警告,并且此类 hackery 中的任何错误都会导致 ClassCastExceptions 出现在对它们进行零转换的行上 - 非常令人困惑。但是,如果您知道自己在做什么,就可以正常工作。您可以使用 @SuppressWarnings 注释消除警告。

或者..不要使用可选的

Java 已 null 融入许多地方。不仅在其核心模型中(未初始化的字段以 null 值开头。non-primitives 的数组用空值初始化),而且在许多最重要的图书馆。最明显和简单的例子是 java.util.Map:它有一个 get() 方法,return 是一个 V。不是 Optional<V>。在 Optional 是正确答案的世界里,get() 当然应该 return Optional<V>。但它不会,而且永远不会 - java 不会彻底破坏向后兼容性。

因此,'optional everywhere!' 的假设世界糟透了 - 30 年的代码和历史需要先扔进垃圾桶,而社区不太可能这样做。如果您不喜欢 java 中的 null 处理(并且有很多很好的理由这样做!),那么需要一个解决方案来让现有代码 backwards-compatibly 进行调整,并且 Optional 无法交付。 (不过,无效注释可能可以!好多了;或者只是写得更好 APIs。地图有 .getOrDefault 可用于确保您的地图查询调用永远不会 return null 首先。一旦你开始编写你的代码永远不会产生 null 值,除非你真的想要任何 deref 抛出的尝试(即很少),那么 null 就不再是一个问题。这些对各种 java 核心库类型的更现代的插件使得编写此类代码变得更加容易。并且向接口添加方法是向后兼容的,因此,正如 java.util.Map 所示,现有的东西可以 backwards-compatibly 添加这些)。

与 Optional 相关的最佳做法是仅将其用于最初引入它的原因:作为流 API 终端的 return 值。像 intList.stream().max() return 和 OptionalInt 因为空列表的最大值是多少?它足够好。如果您编写自己的收集器或流终止操作,请使用 Optional。如果这不是您正在做的,请不要使用它。

您提供的解决方法是一个非常简单的答案,但是如果您想避免占位符 Optional,您可以使用 map 将 Optional<InnerNode> 简单地转换为 Optional<Node>

createInnerNode.<Node>map(Function.identity()).or(() -> createLeafNode());

一般来说,这也是您在概念上将 List<Dog> 转换为 List<Animal> 或将任何通用集合转换为 java 中不同通用集合的方式。这有点烦人,但通配符并不能解决问题,因为您希望转换后的集合中的两个 read/write。

or 的签名应该告诉您为什么您的解决方法有效

Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)

注意它是如何使用 ?在供应商中扩展 T。 由于第一个空的 Optional 的 T 是 Node,or 函数可以接受 returns

的供应商
Optional<? extends Node>

并且由于内部节点和叶节点都是 Optional<? extends Node>,这有效。

所以问题是你在某些时候需要 Optional<Node>

我不认为你可以避免做某事,因为 Optional<InnerNode> 从不输入匹配项 Optional<Node> 出于其他答案中列出的原因(以及堆栈溢出的其他问题)。

阅读其他答案并尝试

Optional<? extends Node> node = createInnerNode();

表示T是一个

? extends Node

这使得 or 似乎想要双重嵌套捕获?我无法停止

Bad return type in lambda expression: Optional<capture of ? extends Node> cannot be converted to Optional<? extends capture of ? extends Node>

基本上尝试使用以下代码进行任何转换时

Optional<? extends Node> node = Node.createInnerNode();
node.or(() -> (Optional<? extends Node>) null);