使用流转换和过滤 Java 地图
Transform and filter a Java Map with streams
我有一张 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()
?
是的,您可以将每个条目映射到另一个临时条目,该条目将保存密钥和解析的整数值。然后您可以根据它们的值过滤每个条目。
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();
您可以使用 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
方法来应用转换和谓词,如果可选值仍然存在,我将把它添加到正在收集的地图中.
另一种方法是从转换后的 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
并投射 HashMap
以更改值的类型:
((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
Map
是 IdentityHashMap
并且 mapFactory
创建 HashMap
.
以更少的开销解决问题的一种方法是将映射和过滤向下移动到收集器。
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); });
后者非常紧凑,至少与单线程性能方面的所有其他变体相当。
Guava是你的朋友:
Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);
请记住,output
是 input
的经过转换、过滤的 视图。如果您想独立操作它们,则需要制作副本。
这是 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的开发者
我有一张 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()
?
是的,您可以将每个条目映射到另一个临时条目,该条目将保存密钥和解析的整数值。然后您可以根据它们的值过滤每个条目。
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();
您可以使用 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
方法来应用转换和谓词,如果可选值仍然存在,我将把它添加到正在收集的地图中.
另一种方法是从转换后的 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
并投射 HashMap
以更改值的类型:
((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
Map
是 IdentityHashMap
并且 mapFactory
创建 HashMap
.
以更少的开销解决问题的一种方法是将映射和过滤向下移动到收集器。
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); });
后者非常紧凑,至少与单线程性能方面的所有其他变体相当。
Guava是你的朋友:
Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);
请记住,output
是 input
的经过转换、过滤的 视图。如果您想独立操作它们,则需要制作副本。
这是 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的开发者