Java8:使用Stream / Map-Reduce / Collector将HashMap <X,Y>转换为HashMap <X,Z>


209

我知道如何ListY-> “转换”一个简单的Java Z,即:

List<String> x;
List<Integer> y = x.stream()
        .map(s -> Integer.parseInt(s))
        .collect(Collectors.toList());

现在,我想对地图进行基本相同的操作,即:

INPUT:
{
  "key1" -> "41",    // "41" and "42"
  "key2" -> "42      // are Strings
}

OUTPUT:
{
  "key1" -> 41,      // 41 and 42
  "key2" -> 42       // are Integers
}

解决方案不应限于String-> Integer。就像List上面的示例一样,我想调用任何方法(或构造函数)。

Answers:


372
Map<String, String> x;
Map<String, Integer> y =
    x.entrySet().stream()
        .collect(Collectors.toMap(
            e -> e.getKey(),
            e -> Integer.parseInt(e.getValue())
        ));

它不如列表代码那么好。您不能Map.Entrymap()通话中构造new ,因此工作会混入collect()通话中。


59
您可以替换e -> e.getKey()Map.Entry::getKey。但这只是口味/编程风格的问题。
Holger 2014年

5
实际上,这只是性能问题,您的建议要稍好于lambda“样式”
Jon Burgin

36

以下是Sotirios Delimanolis的答案的一些变化,从(+1)开始非常好。考虑以下:

static <X, Y, Z> Map<X, Z> transform(Map<? extends X, ? extends Y> input,
                                     Function<Y, Z> function) {
    return input.keySet().stream()
        .collect(Collectors.toMap(Function.identity(),
                                  key -> function.apply(input.get(key))));
}

这里有几点。首先是在泛型中使用通配符;这使功能更加灵活。例如,如果您希望输出映射具有一个比输入映射的键超类的键,则必须使用通配符:

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<CharSequence, Integer> output = transform(input, Integer::parseInt);

(也有一个关于地图值的示例,但它确实是人为设计的,我承认拥有Y的有界通配符仅在边缘情况下有用。)

第二点是entrySet,我没有在输入映射上运行流,而是在上运行了它keySet。我认为,这使代码更简洁一些,但其代价是必须从映射中而不是从映射条目中获取值。顺便说一句,我最初key -> key是第一个参数,toMap()由于某种原因,此操作因类型推断错误而失败。像把它(X key) -> key一样更改为有效Function.identity()

另一个变化如下:

static <X, Y, Z> Map<X, Z> transform1(Map<? extends X, ? extends Y> input,
                                      Function<Y, Z> function) {
    Map<X, Z> result = new HashMap<>();
    input.forEach((k, v) -> result.put(k, function.apply(v)));
    return result;
}

这使用Map.forEach()而不是流。我认为这甚至更简单,因为它省去了收集器,而收集器在使用地图时有些笨拙。原因是Map.forEach()将键和值作为单独的参数给出,而流只有一个值-您必须选择使用键还是映射条目作为该值。在不利的一面,这缺乏其他方法的丰富,流畅的优点。:-)


11
Function.identity()也许看起来很酷,但是由于第一个解决方案要求对每个条目进行映射/哈希查找,而其他所有解决方案则不需要,因此我不建议这样做。
Holger 2014年

13

像这样的通用解决方案

public static <X, Y, Z> Map<X, Z> transform(Map<X, Y> input,
        Function<Y, Z> function) {
    return input
            .entrySet()
            .stream()
            .collect(
                    Collectors.toMap((entry) -> entry.getKey(),
                            (entry) -> function.apply(entry.getValue())));
}

Map<String, String> input = new HashMap<String, String>();
input.put("string1", "42");
input.put("string2", "41");
Map<String, Integer> output = transform(input,
            (val) -> Integer.parseInt(val));

使用泛型的好方法。我认为虽然可以改进-请参阅我的答案。
Stuart Marks 2014年

13

Guava的功能Maps.transformValues正是您要寻找的功能,它可以很好地与lambda表达式配合使用:

Maps.transformValues(originalMap, val -> ...)

我喜欢这种方法,但请注意不要将其传递给java.util.Function。由于它期望使用com.google.common.base.Function,因此Eclipse给出了一个无用的错误-它表示Function不适用于Function,这可能会造成混淆:“方法transformValues(Map <K,V1>,Function <?super V1 ,V2>)不适用于参数(Map <Foo,Bar>,Function <Bar,Baz>)“
mskfisher

如果必须传递java.util.Function,则有两个选择。1.通过使用lambda来解决此问题,让Java类型推断将其解决。2.使用类似javaFunction :: apply的方法引用来生成一个新的lambda,类型推断可以确定。



4

总是存在学习目的的替代方法是通过Collector.of()来创建自定义收集器虽然toMap()JDK集电极这里是简洁(+1 这里)。

Map<String,Integer> newMap = givenMap.
                entrySet().
                stream().collect(Collector.of
               ( ()-> new HashMap<String,Integer>(),
                       (mutableMap,entryItem)-> mutableMap.put(entryItem.getKey(),Integer.parseInt(entryItem.getValue())),
                       (map1,map2)->{ map1.putAll(map2); return map1;}
               ));

我以这个自定义收集器作为基础开始,并想补充一点,至少在使用parallelStream()而不是stream()时,应该将binaryOperator重写为类似于的东西,map2.entrySet().forEach(entry -> { if (map1.containsKey(entry.getKey())) { map1.get(entry.getKey()).merge(entry.getValue()); } else { map1.put(entry.getKey(),entry.getValue()); } }); return map1否则在减少时会丢失值。
user691154 '16

3

如果您不介意使用第三方库,则我的cyclops-react lib具有所有JDK Collection类型的扩展,包括Map。我们可以直接使用“地图”运算符直接转换地图(默认情况下,地图会作用于地图中的值)。

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);

bimap可用于同时转换键和值

  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.