使用流,如何映射HashMap中的值?

47
给定一个包含 String, PersonMap,其中 Person 有一个 String getName() 方法,我该如何将 Map<String, Person> 转换为 Map<String, String>,其中的 String 是通过调用 Person::getName() 获得的?
在 Java 8 之前,我会使用
Map<String, String> byNameMap = new HashMap<>();

for (Map.Entry<String, Person> person : people.entrySet()) {
    byNameMap.put(person.getKey(), person.getValue().getName());
}

但我希望使用流和lambda表达式来完成它。

我无法想象如何以函数式风格实现:Map / HashMap没有实现Stream

people.entrySet() 返回一个 Set<Entry<String, Person>>,我可以在其上进行流处理,但是如何向目标映射添加一个新的 Entry<String, String>

4个回答

58

使用Java 8,您可以做到:

Map<String, String> byNameMap = new HashMap<>();
people.forEach((k, v) -> byNameMap.put(k, v.getName());

虽然你最好使用Guava的Maps.transformValues,它包装了原始的Map,并在你调用get时进行转换,这意味着你只有在实际使用值时才需要支付转换成本。

使用Guava看起来像这样:

Map<String, String> byNameMap = Maps.transformValues(people, Person::getName);

编辑:

根据@Eelco的评论(为了完整性),将转换为地图最好使用Collectors.toMap,如下所示:

Map<String, String> byNameMap = people.entrySet()
  .stream()
  .collect(Collectors.toMap(Map.Entry::getKey, (entry) -> entry.getValue().getName());

我接受这个,因为它非常整洁,没有繁琐的循环。Guava解决方案特别整洁。谢谢。 - David Kerr
2
请注意,第一个代码是不可并行化的,因为HashMap不是线程安全的,相反,如果流是并行的,使用收集器/toMap将分割作业。Guava的版本也不能并行化。但如果这不是问题,那么尽管使用这个版本。 - assylias
3
第一个解决方案无法正常工作。另一个使用toMap收集器的答案可以。 - Eelco
1
@Eelco同意,使用Collectors.toMap确实是这样做的方法(当我回答这个问题时,我才刚开始接触Java 8)。 - Nick Holt
1
如果我们可以这样做就太好了:Map<...> newMap = map.mapValues(mapFunction)... - vinntec

27

一种方法是使用 toMap 收集器:

import static java.util.stream.Collectors.toMap;

Map<String, String> byNameMap = people.entrySet().stream()
                                     .collect(toMap(Entry::getKey, 
                                                    e -> e.getValue().getName()));

4
这很不错,但这种方法有什么优势,相比问题中引用的示例呢?我认为原始示例的代码更清晰/可读性更高。 - Charles Goodwin
3
@CharlesGoodwin 可读性还好,一旦习惯了它,尽管for循环可能更易于阅读。Lambda表达式的一个优点是,对于大型映射,您可以使用entrySet().parallelStream()并轻松地将作业并行化。对于简单的用例,循环是可以的。 - assylias
1
@CharlesGoodwin 我猜当你在使用 map 之前添加更多的筛选和转换时,它只有在可读性方面才会得到回报。 - Mark Rotteveel
@asslias:关于parallelStream(),你提出了一个非常好的观点:值得注意。 - David Kerr
@CharlesGoodwin:我同意这种简单转换没有任何好处,但看到如何完成它还是很有趣的。 - David Kerr
@CharlesGoodwin 我认为代码的可读性可能更好,因为它更接近Java 7语法。但正如之前评论者所指出的那样,它具有副作用,因为它更新了“byNameMap”。在函数式方法中,可以消除副作用。 - Stephen Harrison

4

我使用了一些通用代码,但很遗憾我手头的库中没有找到它。

public static <K, V1, V2> Map<K, V2> remap(Map<K, V1> map,
        Function<? super V1, ? extends V2> function) {

    return map.entrySet()
            .stream() // or parallel
            .collect(Collectors.toMap(
                    Map.Entry::getKey, 
                    e -> function.apply(e.getValue())
                ));
}

这与Guava的Maps.transformValues基本相同,但没有其他人提到的缺点。

Map<String, Person> persons = ...;
Map<String, String> byNameMap = remap(persons, Person::getName);

如果您需要在重新映射函数中同时使用键和值,那么第二个版本可以实现这一点。

public static <K, V1, V2> Map<K, V2> remap(Map<K, V1> map,
        BiFunction<? super K, ? super V1, ? extends V2> function) {

    return map.entrySet()
            .stream() // or parallel
            .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> function.apply(e.getKey(), e.getValue())
                ));
}

它可以例如这样使用

Map<String, String> byNameMap = remap(persons, (key, val) -> key + ":" + val.getName());

1

自Java 9以来,您还可以执行以下操作:

Entry<String, String> entry = Map.entry("a", "b");

在你的地图中,它会被这样使用:
Map<String, String> byNameMap = people.entrySet()
  .stream()
  .map(entry -> Map.entry(entry.getKey(), entry.getValue().getName()))
  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue);

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