[英]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
工作,但是您现在可以使用replaceAll
并HashMap
转换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
Map
是IdentityHashMap
并且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.