有没有一种优雅的方法将 Map<P, Optional<Q>> 转换为稀疏 Map<P, Q>?

21

有没有一种优雅的方式将 Map<P, Optional<Q>> 转换为稀疏的 Map<P, Q>

这应该可以工作,但不太好:


Map<P,Optional<Q>> map = ...;
Map<P,Q> map2 = map.entrySet()
                  .stream().filter(e -> e.getValue().isPresent())
                  .collect(Collectors.toMap(e -> e.getKey(), e->e.getValue().get()));

1
也许可以这样写:map.forEach((key, optional) -> optional.ifPresent(value -> map2.put(key, value))); 但是必须先初始化 map2Map<P, Q> map2 = new HashMap<>(); - Lino
4个回答

11

我会说你的方法已经是最优雅的了,我只会做一些轻微的美化并将你收集器中的 e -> e.getKey() 替换为 Entry::getKey。这只是一个小改变,但比其他 lambda 表达了你的意图更好。

Map<P, Optional<Q>> map = new HashMap<>();
Map<P, Q> sparseMap = map.entrySet().stream()
    .filter(e -> e.getValue().isPresent())
    .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().get()));

为什么其他解决方案不更好/更优雅?
因为它们不够简洁,并且它们再次陷入了一个陷阱,即没有声明你想要做什么,而是如何做,这在过程式编程风格中很常见,但在函数式编程中则不是那么常见。
如果您查看上面的代码,它几乎是自说明的,并且具有良好的流程。您首先有一个带有 Optional 的非稀疏映射,然后声明没有 Optional 的稀疏映射,然后描述前者映射到后者的转换。这也没有副作用。只有在收集器实际完成时才会分配稀疏映射。
如果您查看其他解决方案,则将反转逻辑流,并使用过程式思维方式。
Map<P, Optional<Q>> map = [....];
Map<P, Q> sparseMap = new HashMap<>();
map.forEach((key, opt) -> opt.ifPresent(value -> sparseMap.put(key, value)));

这只比以下内容稍微短一些:

Map<P, Optional<Q>> map = [....];
Map<P, Q> sparseMap = new HashMap<>();
for (Entry<P, Optional<Q>> e : map.entrySet()) e.getValue().ifPresent(value -> sparseMap.put(key, value))

由于类型推断,使用 foreach 方案可以节省一些字符,但最终,如果格式合理,两种 foreach 解决方案都需要 4 行代码,因此它们并不比函数式的解决方案更短。它们也不更清晰。相反,它们依赖于在其他 map 中引起副作用。这意味着在计算过程中,会将部分构造好的稀疏 map 分配给您的变量。使用函数式的解决方案时,只有在正确构造 map 时才会进行分配。这只是小小的一点问题,在这种情况下可能不会引起问题,但在其他情况下可能会产生影响(例如当涉及并发性时),特别是当另一个 map 不是局部变量而是字段 — 或者更糟糕时,从其他地方传递而来。

此外,函数式方法具有更好的可扩展性-如果您有大量数据,并行流的切换非常简单,将 foreach-方法转换为并行需要重写为函数式的 filter/collect 方法。虽然在这种轻量级操作中并不相关(实际上,在这里不要这么做,它可能会更慢),但在其他情况下,这可能是一个可取的特征。

在我看来,使用函数式的 filter/collect 方法比使用过程式的 foreach 更可取,因为您可以养成好习惯。但请记住,“优美”常常在观察者的眼中。对我而言,更“优美”的方式是适当的函数式方式,它没有副作用。但你可能会有不同的看法。


3
虽然并行化流的能力在理论上很好,但我非常希望看到并行流比顺序流(或直接迭代映射)更有效地执行此任务的映射。一般来说,当条目必须经过一些缓慢的非平凡处理,这些处理可以在多个线程之间有用地共享时,并行流是有用的。然而,仅提取Optional中的值就是一种轻量级操作,其并行化的开销可能会大大超过操作本身的成本。 - Ilmari Karonen
为什么不在收集成映射之前,通过映射将 Entry<P, Optional<Q>> 转换为 Entry<P, Q>? - Édouard
“map.forEach”仅比“for”循环略短,因为您正在使用原始类型“Entry”。正确的解决方案必须使用“Entry<P,Optional<Q>>”,因此代码大小的差异取决于实际的“P”和“Q”。 - Holger
@IlmariKaronen 绝对没错,这与情况无关。这就是我指出要养成好习惯的原因,因为当你陷入需要它的情况时,你不需要不同于你已经编写的代码。 - Polygnome
@Holger 没错,我会稍微修改一下。 - Polygnome

10

这样怎么样:

Map<P, Q> map2 = new HashMap<>();
map.forEach((key, opt) -> opt.ifPresent(value -> map2.put(key, value)));

6

如果您创建一个新的映射并通过迭代原始映射添加值,那将更加简单:

Map<P, Q> map2 = new HashMap<>();
map.forEach((p, q) -> map2.compute(p, (k, v) -> q.orElse(null)));

所有原始条目中,value.isPresent() 返回 false 的条目都会被跳过(不包含在 result 中):当 remappingFunction 返回 null 时,Map.compute 也会移除/忽略键的映射。


以下示例将说明如何处理 null 值:

Map<String, Optional<Integer>> map = new HashMap<>();
map.put("one", Optional.of(1));
map.put("two", Optional.of(2));
map.put("three", Optional.empty());
map.put("four", Optional.of(4));
map.put("five", Optional.empty());

Map<String, Integer> map2 = new HashMap<>();
map.forEach((p, q) -> map2.compute(p, (k, v) -> q.orElse(null)));

System.out.println(map2);

输出结果为{four=4, one=1, two=2}(当map2.computeq.orElse获得null时,p不会被添加到map2)。

@ernest_k 还是null看起来有点不好。如果可能的话,我会避免使用它。 - ZhekaKozlov
@ZhekaKozlov 我可能没有解释清楚。它不是插入空值。我会添加一个例子。 - ernest_k
1
这段代码很短,但我觉得它不太易读。如果我在一个更大的代码库中看到它,它的作用可能不会立即显而易见。 - John Kugelman
@JohnKugelman 感谢您的意见。可读性既是相对的,有时也是次要的。 - ernest_k
@IlmariKaronen 这是 Stack Overflow 最好的地方之一;-) - 一个问题能得到很多种答案,有些在某种或多种方式下比其他答案更好。这就是为什么有时候即使是和我们自己的答案竞争的答案也会让我们无法抵制投赞成票的原因。这就是我们学习或者发现其他视角的方式。这就是我们在这里学习的方式:通过看到正确和错误的示例(借助赞成和反对的论据)。毕竟这不是一场竞赛... - ernest_k
显示剩余5条评论

3

我建议你坚持现有的方案。它直接表达了意图,而且利用了流式传输这一特性,其他答案都没有。有时候 Stream 会比较冗长,但也无可奈何。

Map<P,Q> map2 = map.entrySet()
                  .stream().filter(e -> e.getValue().isPresent())
                  .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().get()));

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