简体   繁体   English

使用流转换和过滤Java Map

[英]Transform and filter a Java Map with streams

I have a Java Map that I'd like to transform and filter. 我有一个我想要转换和过滤的Java Map。 As a trivial example, suppose I want to convert all values to Integers then remove the odd entries. 作为一个简单的例子,假设我想将所有值转换为整数然后删除奇数条目。

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());

This is correct and yields: {a=1234, c=3456} 这是正确的并且产生: {a=1234, c=3456}

However, I can't help but wonder if there's a way to avoid calling .entrySet().stream() twice. 但是,我不禁想知道是否有办法避免两次调用.entrySet().stream()

Is there a way I can perform both transform and filter operations and call .collect() only once at the end? 有没有办法可以执行转换和过滤操作,并且.collect()只调用一次.collect()一次?

Yes, you can map each entry to another temporary entry that will hold the key and the parsed integer value. 是的,您可以将每个条目映射到另一个临时条目,该条目将保存密钥和解析的整数值。 Then you can filter each entry based on their value. 然后,您可以根据其值过滤每个条目。

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
         ));

Note that I used Integer.valueOf instead of parseInt since we actually want a boxed int . 请注意,我使用Integer.valueOf而不是parseInt因为我们实际上想要一个盒装的int


If you have the luxury to use the StreamEx library, you can do it quite simply: 如果您有幸使用StreamEx库,您可以非常简单地完成:

Map<String, Integer> output =
    EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();

One way to solve the problem with much lesser overhead is to move the mapping and filtering down to the collector. 以较小的开销解决问题的一种方法是将映射和过滤向下移动到收集器。

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);

This does not require the creation of intermediate Map.Entry instances and even better, will postpone the boxing of int values to the point when the values are actually added to the Map , which implies that values rejected by the filter are not boxed at all. 这不需要创建中间Map.Entry实例,甚至更好,将int值的装箱推迟到值实际添加到Map时的点,这意味着过滤器拒绝的值根本没有装箱。

Compared to what Collectors.toMap(…) does, the operation is also simplified by using Map.put rather than Map.merge as we know beforehand that we don't have to handle key collisions here. Collectors.toMap(…)相比,通过使用Map.put而不是Map.merge来简化操作,因为我们事先已知道我们不必在这里处理关键冲突。

However, as long as you don't want to utilize parallel execution you may also consider the ordinary loop 但是,只要您不想使用并行执行,您也可以考虑普通循环

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);
}

or the internal iteration variant: 或内部迭代变体:

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

the latter being quite compact and at least on par with all other variants regarding single threaded performance. 后者非常紧凑,至少与所有其他有关单螺纹性能的变体相同。

Guava 's your friend: 番石榴是你的朋友:

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

Keep in mind that output is a transformed, filtered view of input . 请记住, output是转换的,过滤的input 视图 You'll need to make a copy if you want to operate on them independently. 如果您想独立操作它们,您需要制作副本。

You could use the Stream.collect(supplier, accumulator, combiner) method to transform the entries and conditionally accumulate them: 您可以使用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}

Here, inside the accumulator, I'm using Optional methods to apply both the transformation and the predicate, and, if the optional value is still present, I'm adding it to the map being collected. 在这里,在累加器中,我使用Optional方法来应用转换和谓词,如果可选值仍然存在,我将它添加到正在收集的地图中。

Another way to do this is to remove the values you don't want from the transformed Map : 另一种方法是从转换后的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);

This assumes you want a mutable Map as the result, if not you can probably create an immutable one from output . 这假设你想要一个可变的Map作为结果,如果不是,你可以从output创建一个不可变的Map


If you are transforming the values into the same type and want to modify the Map in place this could be alot shorter with replaceAll : 如果要将值转换为相同的类型并想要修改Map ,则使用replaceAll可能会更短:

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

This also assumes input is mutable. 这也假设input是可变的。


I don't recommend doing this because It will not work for all valid Map implementations and may stop working for HashMap in the future, but you can currently use replaceAll and cast a HashMap to change the type of the values: 我不建议这样做,因为它不适用于所有有效的Map实现,并且可能在将来停止为HashMap工作,但是您现在可以使用replaceAllHashMap转换HashMap来更改值的类型:

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

This will also give you type safety warnings and if you try to retrieve a value from the Map through a reference of the old type like this: 这也将为您提供类型安全警告,如果您尝试通过旧类型的引用从Map检索值,如下所示:

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

It will throw a ClassCastException . 它将抛出ClassCastException


You could move the first transform part into a method to avoid the boilerplate if you expect to use it alot: 如果您希望使用它,可以将第一个转换部分移动到避免样板的方法中:

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));
}

And use it like this: 并像这样使用它:

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

Note that the duplicate key exception can be thrown if, for example, the old Map is an IdentityHashMap and the mapFactory creates a HashMap . 请注意,例如,如果old MapIdentityHashMap并且mapFactory创建HashMap ,则可以抛出重复键异常。

Here is code by AbacusUtil 这是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());

Declaration: I'm the developer of AbacusUtil. 声明:我是AbacusUtil的开发人员。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM