50

我有一个想要转换和过滤的 Java 地图。作为一个简单的例子,假设我想将所有值转换为整数,然后删除奇数条目。

Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue())
        ))
        .entrySet().stream()
        .filter(e -> e.getValue() % 2 == 0)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


System.out.println(output.toString());

这是正确的并且产生:{a=1234, c=3456}

但是,我不禁想知道是否有办法避免调用.entrySet().stream()两次。

有没有一种方法可以同时执行转换和过滤操作, .collect()最后只调用一次?

4

6 回答 6

62

是的,您可以将每个条目映射到另一个临时条目,该条目将保存键和解析的整数值。然后,您可以根据每个条目的值过滤它们。

Map<String, Integer> output =
    input.entrySet()
         .stream()
         .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
         .filter(e -> e.getValue() % 2 == 0)
         .collect(Collectors.toMap(
             Map.Entry::getKey,
             Map.Entry::getValue
         ));

请注意,我使用Integer.valueOf而不是parseInt因为我们实际上想要一个盒装的int.


如果你有幸使用StreamEx库,你可以很简单地做到这一点:

Map<String, Integer> output =
    EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
于 2016-02-18T16:27:33.087 回答
18

以更少的开销解决问题的一种方法是将映射和过滤下移到收集器。

Map<String, Integer> output = input.entrySet().stream().collect(
    HashMap::new,
    (map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
    Map::putAll);

这不需要创建中间Map.Entry实例,甚至更好的是,将值的装箱推迟int到值实际添加到 的点Map,这意味着过滤器拒绝的值根本不会装箱。

与做什么相比Collectors.toMap(…),操作也通过使用Map.put而不是Map.merge我们事先知道的我们不必在这里处理键冲突来简化。

但是,只要您不想使用并行执行,您也可以考虑使用普通循环

HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
    int i = Integer.parseInt(e.getValue());
    if(i%2==0) output.put(e.getKey(), i);
}

或内部迭代变体:

HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });

后者非常紧凑,至少在单线程性能方面与所有其他变体相当。

于 2016-02-18T19:20:07.393 回答
6

番石榴是你的朋友:

Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);

请记住,这是一个经过转换的output过滤视图input如果您想独立操作它们,您需要制作一份副本。

于 2016-02-19T05:22:58.787 回答
4

您可以使用该Stream.collect(supplier, accumulator, combiner)方法来转换条目并有条件地累积它们:

Map<String, Integer> even = input.entrySet().stream().collect(
    HashMap::new,
    (m, e) -> Optional.ofNullable(e)
            .map(Map.Entry::getValue)
            .map(Integer::valueOf)
            .filter(i -> i % 2 == 0)
            .ifPresent(i -> m.put(e.getKey(), i)),
    Map::putAll);

System.out.println(even); // {a=1234, c=3456}

在这里,在累加器中,我使用Optional方法来应用转换和谓词,如果可选值仍然存在,我将把它添加到正在收集的地图中。

于 2016-02-18T19:15:59.663 回答
3

另一种方法是从转换后删除您不想要的值Map

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue()),
                (a, b) -> { throw new AssertionError(); },
                HashMap::new
         ));
output.values().removeIf(v -> v % 2 != 0);

这假设您想要一个可变Map的结果,如果不是,您可能可以从output.


如果您要将值转换为相同的类型并想要修改Map就地,这可能会更短replaceAll

input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);

这也假设input是可变的。


我不建议这样做,因为它不适用于所有有效的Map实现,并且将来可能会停止工作HashMap,但您目前可以使用replaceAll并转换 aHashMap来更改值的类型:

((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);

这也会给你类型安全警告,如果你尝试Map通过旧类型的引用从 中检索一个值,如下所示:

String ex = input.get("a");

它会抛出一个ClassCastException.


如果您希望大量使用它,您可以将第一个转换部分移动到一个方法中以避免样板:

public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
        Map<? extends K, ? extends VO> old, 
        Function<? super VO, ? extends VN> f, 
        Supplier<? extends M> mapFactory){
    return old.entrySet().stream().collect(Collectors.toMap(
            Entry::getKey, 
            e -> f.apply(e.getValue()), 
            (a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
            mapFactory));
}

并像这样使用它:

    Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
    output.values().removeIf(v -> v % 2 != 0);

请注意,例如,如果old Mapis anIdentityHashMap并且mapFactory创建 a ,则可能会引发重复键异常HashMap

于 2016-02-18T19:20:00.283 回答
0

这是AbacusUtil的代码

Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");

Map<String, Integer> output = Stream.of(input)
                          .groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
                          .filter(e -> e.getValue() % 2 == 0)
                          .toMap(Map.Entry::getKey, Map.Entry::getValue);

N.println(output.toString());

声明:我是AbacusUtil的开发者。

于 2016-11-29T22:39:24.340 回答