Java 8:使用流/映射-归约/收集器将HashMap<X,Y>转换为HashMap<X,Z>

283

我知道如何将一个简单的Java ListY转换为Z,即:

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

现在我想用一个Map基本上做同样的事情,即:

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

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

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

10个回答

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

和列表代码相比,这段代码不是很好。在map()调用中无法构建新的Map.Entry,因此工作被混合到collect()调用中。


72
你可以用Map.Entry::getKey来代替e -> e.getKey(),但这只是个人口味和编程风格的问题。 - Holger
7
实际上这是一个关于性能的问题,你的建议略微优于lambda“风格”。 - Jon Burgin

38

以下是对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))));
}

这里有几个要点。首先是在泛型中使用通配符,这使得函数更加灵活。如果你想让输出的映射表的键是输入映射表键的超类,那么就需要使用通配符:

A couple points here. First is the use of wildcards in the generics; this makes the function somewhat more flexible. A wildcard would be necessary if, for example, you wanted the output map to have a key that's a superclass of the input map's 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() 而不是流(streams)。我认为这甚至更简单,因为它省去了与 Map 一起使用有些笨拙的收集器(collectors)的步骤。原因是 Map.forEach() 输出键和值作为分开的参数,而流只有一个值--你必须选择使用键还是地图条目作为该值。缺点是,这种方法缺乏其他方法中丰富的流优美性质。:-)


11
Function.identity() 看起来很酷,但是由于第一个解决方案需要为每个条目进行一次映射/哈希查找,而其他所有解决方案都不需要,所以我不建议使用它。 - Holger

19
一个通用的解决方案就是这样。
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));

16

Guava的函数Maps.transformValues就是你正在寻找的东西,它可以很好地与lambda表达式配合使用:

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

我喜欢这种方法,但要小心不要传递java.util.Function。由于它期望com.google.common.base.Function,因此Eclipse会给出一个无用的错误 - 它说Function不适用于Function,这可能会令人困惑:“Maps中的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,让类型推断来解决问题。 - Joe
请注意,与此页面上的其他解决方案不同,此解决方案返回底层地图的视图,而不是副本。 - Lawrence Kesteloot

12
它一定要百分之百地功能完善和流利吗?如果不是的话,那么这个翻译可能就足够简洁了。
Map<String, Integer> output = new HashMap<>();
input.forEach((k, v) -> output.put(k, Integer.valueOf(v));

4
我的StreamEx库增强了标准流API,并提供了一个EntryStream类,更适合于转换映射。可以在此链接上查看StreamEx库,或访问此处查看EntryStream类的文档。请注意,HTML标签已保留。
Map<String, Integer> output = EntryStream.of(input).mapValues(Integer::valueOf).toMap();

3

为了学习目的,一种可行的替代方法是通过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

3

虽然在流的collect部分中重新映射键或/和值是可能的,如其他答案所示,但我认为它应该属于map部分,因为该函数旨在转换流中的数据。此外,它应该很容易地重复使用,而不会引入额外的复杂性。可以使用SimpleEntry对象,它自Java 6以来已经可用。

使用Java 8

import java.util.AbstractMap.SimpleEntry;
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, String> x;
        Map<String, Integer> y = x.entrySet().stream()
                .map(entry -> new SimpleEntry<>(entry.getKey(), Integer.parseInt(entry.getValue())))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
    }

}

使用Java 9+

随着Java 9的发布,Map接口中引入了一个静态方法,使得创建一个条目变得更加容易,无需像之前的示例一样实例化一个新的SimpleEntry。

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, String> x;
        Map<String, Integer> y = x.entrySet().stream()
                .map(entry -> Map.entry((entry.getKey(), Integer.parseInt(entry.getValue())))
                .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
    }

}

2

如果您不介意使用第三方库,我的cyclops-react库已经扩展了所有JDK集合类型,包括Map。 我们可以直接使用'map'操作符(默认情况下是针对map中的值)来转换map。

   MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                                .map(Integer::parseInt);
可以同时转换键和值。
  MapX<String,Integer> y = MapX.fromMap(HashMaps.of("hello","1"))
                               .bimap(this::newKey,Integer::parseInt);

-2

  1. 请将代码放入代码块中。
  2. 修改输入通常不是一个好习惯,最好是创建一个新对象。
- bladefist

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