Java8使用Stream API转换Map遇到的2种异常报错和解决思路

Posted cdfive

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8使用Stream API转换Map遇到的2种异常报错和解决思路相关的知识,希望对你有一定的参考价值。

问题

java8提供了Stream API,配合Lambda表达式,让开发者能对集合对象进行便利、高效的操作。
在日常业务开发中,有个经常用到的场景是将List类型对象转换为Map类型对象,方便后续操作。
在java8之前,这种转换需要先new一个Map对象,遍历list然后通过Map#put来初始化。
使用java8后,可方便的使用list.stream().collect(Collectors.toMap(...))进行转换。
然而这种转换可能会遇到转换失败程序报错的情况,这里总结了常见的2种报错的例子和解决思路。

示例和分析

1.java.lang.IllegalStateException

场景:有重复的key

例:

public static void main(String[] args) 
    Person p1 = new Person("aa", 18);
    Person p2 = new Person("bb", 20);
    Person p3 = new Person("cc", 18);
    List<Product> list = Stream.of(p1, p2, p3).collect(Collectors.toList());

    Map<Integer, Person> map = list.stream().collect(Collectors.toMap(p -> p.getAge(), p -> p);
    System.out.println(map);


@NoArgsConstructor
@AllArgsConstructor
@Data
static class Person 

    private String name;

    private Integer age;

Collectors.toMap里使用里使用Person类的age字段为key,由于p1p3age值都为18,上面程序运行错误信息如下:

Exception in thread "main" java.lang.IllegalStateException: Duplicate key StreamToMapTest1.Person(name=aa, age=18)
	at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)
	at java.util.HashMap.merge(HashMap.java:1254)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)

解决思路:使用Collectors.toMap的3个参数的重载方法,第3个参数指定mergeFunction

Map<Integer, Person> map = list.stream().collect(Collectors.toMap(p -> p.getAge(), p -> p, (o, n) -> n));

(o, n) -> n)表示遇到重复需要合并时使用新值;
如果写(o, n) -> o)则是使用旧值;
o和n是old和new的缩写,便于理解,也可以用其它变量名。

2.java.lang.NullPointerException

场景:value里有null值

例:

public static void main(String[] args) 
    Product p1 = new Product("1001", "aaa");
    Product p2 = new Product("1002", null);
    Product p3 = new Product("1003", "bbb");
    List<Product> list = Stream.of(p1, p2, p3).collect(Collectors.toList());

    Map<String, String> map = list.stream().collect(Collectors.toMap(p -> p.getProductCode(), p -> p.getBarCode(), (o, n) -> n));
    System.out.println(map);


@NoArgsConstructor
@AllArgsConstructor
@Data
private static class Product 

    private String productCode;

    private String barCode;

Collectors.toMap里使用里使用Product类的barCode字段为value,由于p2barCode值是null,上面程序运行错误信息如下:

Exception in thread "main" java.lang.NullPointerException
	at java.util.HashMap.merge(HashMap.java:1225)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)

解决思路:
1.考虑把value为null的通过filter过滤掉再转换map
注:这是思路适用于不需要null的场景,可能有的场景map里需要保留null值,然后对map做进一步处理,可考虑思路2。

2.使用stream().collect的重载方法来创建Map

HashMap<Object, Object> map = list.stream().collect(HashMap::new, (m, p) -> m.put(p.getProductCode(), p.getBarCode()), HashMap::putAll);

参考

使用 Java 8 Stream API 合并两个 Map<String, Integer>

【中文标题】使用 Java 8 Stream API 合并两个 Map<String, Integer>【英文标题】:Merging two Map<String, Integer> with Java 8 Stream API 【发布时间】:2014-05-27 03:34:00 【问题描述】:

我有两个(或更多)Map&lt;String, Integer&gt; 对象。我想将它们与 Java 8 Stream API 合并,使公共键的值应该是值的最大值。

@Test
public void test14() throws Exception 
    Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3);
    Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4);
    List<Map<String, Integer>> list = newArrayList(m1, m2);

    Map<String, Integer> mx = list.stream()... // TODO

    Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4);
    assertEquals(expected, mx);

我怎样才能使这个测试方法变成绿色?

我和collectCollectors 玩了一段时间没有任何成功。

ImmutableMapnewArrayList 来自 Google Guava。)

【问题讨论】:

【参考方案1】:
@Test
public void test14() throws Exception 
    Map<String, Integer> m1 = ImmutableMap.of("a", 2, "b", 3);
    Map<String, Integer> m2 = ImmutableMap.of("a", 3, "c", 4);

    Map<String, Integer> mx = Stream.of(m1, m2)
        .map(Map::entrySet)          // converts each map into an entry set
        .flatMap(Collection::stream) // converts each set into an entry stream, then
                                     // "concatenates" it in place of the original set
        .collect(
            Collectors.toMap(        // collects into a map
                Map.Entry::getKey,   // where each entry is based
                Map.Entry::getValue, // on the entries in the stream
                Integer::max         // such that if a value already exist for
                                     // a given key, the max of the old
                                     // and new value is taken
            )
        )
    ;

    /* Use the following if you want to create the map with parallel streams
    Map<String, Integer> mx = Stream.of(m1, m2)
        .parallel()
        .map(Map::entrySet)          // converts each map into an entry set
        .flatMap(Collection::stream) // converts each set into an entry stream, then
                                     // "concatenates" it in place of the original set
        .collect(
            Collectors.toConcurrentMap(        // collects into a map
                Map.Entry::getKey,   // where each entry is based
                Map.Entry::getValue, // on the entries in the stream
                Integer::max         // such that if a value already exist for
                                     // a given key, the max of the old
                                     // and new value is taken
            )
        )
    ;
    */

    Map<String, Integer> expected = ImmutableMap.of("a", 3, "b", 3, "c", 4);
    assertEquals(expected, mx);

【讨论】:

太棒了!我只需要不同的东西,而不是最大值,我需要平均值。我该怎么做? @FirasAlMannaa docs.oracle.com/javase/8/docs/api/java/util/stream/…【参考方案2】:
Map<String, Integer> mx = new HashMap<>(m1);
m2.forEach((k, v) -> mx.merge(k, v, Integer::max));

【讨论】:

【参考方案3】:
mx = list.stream().collect(HashMap::new,
        (a, b) -> b.forEach((k, v) -> a.merge(k, v, Integer::max)),
        Map::putAll);

这涵盖了任何大小列表的一般情况,应该适用于任何类型,只需根据需要换出Integer::max 和/或HashMap::new

如果您不关心合并中出现的值,那么有一个更简洁的解决方案:

mx = list.stream().collect(HashMap::new, Map::putAll, Map::putAll);

作为通用方法:

public static <K, V> Map<K, V> mergeMaps(Stream<? extends Map<K, V>> stream) 
    return stream.collect(HashMap::new, Map::putAll, Map::putAll);


public static <K, V, M extends Map<K, V>> M mergeMaps(Stream<? extends Map<K, V>> stream,
        BinaryOperator<V> mergeFunction, Supplier<M> mapSupplier) 
    return stream.collect(mapSupplier,
            (a, b) -> b.forEach((k, v) -> a.merge(k, v, mergeFunction)),
            Map::putAll);

【讨论】:

【参考方案4】:

我为任何可能感兴趣的人创建了 @srborlongan 所做工作的可视化表示。

【讨论】:

【参考方案5】:

我将我的贡献添加到 proton pack library,其中包含 Stream API 的实用方法。您可以通过以下方式实现您想要的:

Map<String, Integer> mx = MapStream.ofMaps(m1, m2).mergeKeys(Integer::max).collect();

基本上mergeKeys 将在新映射中收集键值对(如果合并功能是可选的,否则您将得到一个Map&lt;String, List&lt;Integer&gt;&gt;)并在entrySet() 上调用stream() 以获得一个新的MapStream。然后使用collect() 得到结果图。

【讨论】:

【参考方案6】:

使用StreamEx 你可以做到:

StreamEx.of(m1, m2)
    .flatMapToEntry(x -> x)
    .grouping(IntCollector.max())

【讨论】:

以上是关于Java8使用Stream API转换Map遇到的2种异常报错和解决思路的主要内容,如果未能解决你的问题,请参考以下文章

Java8: Stream API

Java8 - Streams map()

Java8 - Streams map()

关于Java8 stream的相关使用及解析

通俗易懂,java8 .stream().map().collect()用法

通俗易懂,java8 .stream().map().collect()用法