在Java 8中,如何使用lambda表达式将一个Map<K,V>转换为另一个Map<K,V>?

209

我刚开始学习Java 8,为了试用lambda表达式,我想重新编写一些我最近写过的非常简单的东西。我需要将一个String到Column的Map转换为另一个String到Column的Map,其中新Map中的Column是第一个Map中Column的防御性副本。Column具有复制构造函数。到目前为止,我得到的最接近的代码是:

    Map<String, Column> newColumnMap= new HashMap<>();
    originalColumnMap.entrySet().stream().forEach(x -> newColumnMap.put(x.getKey(), new Column(x.getValue())));

但我相信一定有更好的方法来完成它,如果能得到一些建议,我将不胜感激。

8个回答

328
您可以使用一个收集器(Collector):
import java.util.*;
import java.util.stream.Collectors;

public class Defensive {

  public static void main(String[] args) {
    Map<String, Column> original = new HashMap<>();
    original.put("foo", new Column());
    original.put("bar", new Column());

    Map<String, Column> copy = original.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey,
                                  e -> new Column(e.getValue())));

    System.out.println(original);
    System.out.println(copy);
  }

  static class Column {
    public Column() {}
    public Column(Column c) {}
  }
}

17
我认为上述内容中需要注意的重要(并且有点违反直觉的)一点是,转换发生在收集器中,而不是在map()流操作中。 - Brian Agnew
是的,我希望它可以在收集器之外完成。它感觉不同于其他类型的转换。 - Sridhar Sarnobat

35
Map<String, Integer> map = new HashMap<>();
map.put("test1", 1);
map.put("test2", 2);

Map<String, Integer> map2 = new HashMap<>();
map.forEach(map2::put);

System.out.println("map: " + map);
System.out.println("map2: " + map2);
// Output:
// map:  {test2=2, test1=1}
// map2: {test2=2, test1=1}

您可以使用forEach方法来实现您想要的功能。

您正在做的是:

map.forEach(new BiConsumer<String, Integer>() {
    @Override
    public void accept(String s, Integer integer) {
        map2.put(s, integer);     
    }
});

我们可以将其简化为一个 lambda 表达式:

map.forEach((s, integer) ->  map2.put(s, integer));

由于我们只是在调用现有的方法,所以我们可以使用方法引用,这样就可以得到:

map.forEach(map2::put);

9
我喜欢您对幕后情况的解释,这使它更清晰易懂。但我觉得这只是给了我原始地图的直接副本,而不是将值转换为防御副本的新地图。我的理解是,我们之所以可以使用(map2 :: put)是因为相同的参数进入lambda,例如(s,integer),如同进入put方法一样。那么,要进行防御性拷贝,需要是“originalColumnMap.forEach((string,column) - > newColumnMap.put(string,new Column(column)))”吗?还是可以更简短? - annesadleir
是的,如果你只是传递相同的参数,你可以使用方法引用,但在这种情况下,由于你有new Column(column)作为参数,你必须使用它。 - Arrem
我更喜欢这个答案,因为它适用于如果两个地图已经提供给您的情况,例如在会话续订中。 - David Stanley
不确定理解这种回答的意义。它重新编写了大多数Map实现的构造函数,从现有的Map中获取 - 就像这样 - Kineolyan

21

保持简单并使用Java 8:-

 Map<String, AccountGroupMappingModel> mapAccountGroup=CustomerDAO.getAccountGroupMapping();
 Map<String, AccountGroupMappingModel> mapH2ToBydAccountGroups = 
              mapAccountGroup.entrySet().stream()
                         .collect(Collectors.toMap(e->e.getValue().getH2AccountGroup(),
                                                   e ->e.getValue())
                                  );

在这种情况下,map.forEach(map2::put); 很简单 :) - ses

15

不需要将所有条目重新插入新的映射表中的方法应该是最快的,但实际上它不会,因为HashMap.clone内部也会执行重新哈希操作。

Map<String, Column> newColumnMap = originalColumnMap.clone();
newColumnMap.replaceAll((s, c) -> new Column(c));

这是一种非常易读且相当简洁的方法。除了HashMap.clone()之外,似乎还有Map<String,Column> newColumnMap = new HashMap<>(originalColumnMap);。我不知道在底层是否有任何不同。 - annesadleir
2
这种方法的优点是保持了Map实现的行为,即如果MapEnumMapSortedMap,那么结果也将是相同的。对于具有特殊ComparatorSortedMap来说,这可能会产生巨大的语义差异。嗯,对于IdentityHashMap也是一样的。 - Holger

8
如果你的项目中使用了Guava(最低版本为v11),你可以使用Maps::transformValues。它可以用于转换Map中的所有值。详见Maps::transformValues
Map<String, Column> newColumnMap = Maps.transformValues(
  originalColumnMap,
  Column::new // equivalent to: x -> new Column(x) 
)

注意:此 Map 的值是惰性求值的。如果转换代价昂贵,建议像 Guava 文档中提到的那样将结果复制到一个新的 Map 中。
To avoid lazy evaluation when the returned map doesn't need to be a view, copy the returned map into a new map of your choosing.

注意:根据文档,这返回原始列映射的惰性评估视图,因此每次访问条目时都会重新评估Column :: new函数(当映射函数很昂贵时可能不希望这样做) - Erric
如果转换是昂贵的,那么按照文档中建议的将其复制到新地图的开销可能是可以接受的。为了避免在返回的地图不需要视图时进行惰性评估,请将返回的地图复制到您选择的新地图中。 - Andrea Bergonzo
确实如此,但对于那些倾向于跳过阅读文档的人来说,将其作为答案的脚注可能是值得添加的。 - Erric
@Erric 没问题,已添加。 - Andrea Bergonzo

6
这里有另一种方式,可以同时访问键和值,以便进行某种转换。
Map<String, Integer> pointsByName = new HashMap<>();
Map<String, Integer> maxPointsByName = new HashMap<>();

Map<String, Double> gradesByName = pointsByName.entrySet().stream()
        .map(entry -> new AbstractMap.SimpleImmutableEntry<>(
                entry.getKey(), ((double) entry.getValue() /
                        maxPointsByName.get(entry.getKey())) * 100d))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

是的,我最终做到了这一点,但并不是很满意。 - Sridhar Sarnobat

1
从Java 9开始,在流的map部分内进行转换变得更加容易。使用new AbstractMap.SimpleImmutableEntry已经是可能的,但Map接口还有一个额外的静态方法Map.entry,可以创建一个条目,用于此用例。

Java 9+

import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;

public class App {

    public static void main(String[] args) {
        Map<String, Column> x;
        Map<String, Column> y = x.entrySet().stream()
                .map(entry -> Map.entry((entry.getKey(), new Column(entry.getValue())))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
    }

}

1
如果您不介意使用第三方库,我的cyclops-react库具有所有JDK Collection类型的扩展,包括Map。您可以直接使用map或bimap方法来转换您的Map。可以从现有的Map构建一个MapX。
  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .map(c->new Column(c.getValue());

如果你想改变密钥,你也可以写:

,保留HTML格式。
  MapX<String, Column> y = MapX.fromMap(orgColumnMap)
                               .bimap(this::newKey,c->new Column(c.getValue());

由于MapX扩展了Map,因此生成的映射也可以定义为

  Map<String, Column> y

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