How can I flatten a Stream of Maps (of the same types) to a single Map in Java 8?
Map<String, Long> toMap(Stream<Map<String, Long>> stream) {
return stream. ???
}
My syntax may be a bit off, but flatMap should do most of the work for you :
Map<String, Long> toMap(Stream<Map<String, Long>> stream) {
return stream.flatMap (map -> map.entrySet().stream()) // this would create a flattened
// Stream of all the map entries
.collect(Collectors.toMap(e -> e.getKey(),
e -> e.getValue())); // this should collect
// them to a single map
}
I would like to propose a solution using reduce(), which is more intuitive for me. I would use it inline though.
Map<String, Long> toMap(Stream<Map<String, Long>> stream) {
return stream.reduce(new HashMap<>(), Util::reduceInto);
}
And in Util.java:
public static <R, T> Map<R, T> reduceInto(Map<R, T> into, Map<R, T> valuesToAdd) {
reduceInto.putAll(valuesToAdd);
return reduceInto;
}
In this case reduceInto() works for any type of map and uses mutability to avoid creating a new Map for each item of the Stream.
Important: although this method allows repeated keys in the stream, reduceInto() is not associative, meaning that if you have repeated keys there is no guarantee of which will be the final value.
Related
I am currently getting a map data from an external api call.
I want to ensure the data is not null or empty and perform a set of operations on it
by filtering to a specific key in the map and capturing results into another object.
The key itself is comma separated.
Example key / value in map.
"key1,key2,key3,id100" : {
"val1: "",
"val2: "",
"val3: "",
... others
}
I am filtering to capture all values under this key (so data cal1, val2, val3 and others)
and then perform some operations.
But when I perform the filter as shown, I end up with a stream.
Thus Instead of just a Map<String, Object>, I end up with Stream<Map.Entry<String, Object>>.
Tried flatmap and getting following error:
no instance(s) of type variable(s) U exist so that
Stream<Entry<String, Object>> conforms to Optional
How could I convert it back to a Map from the Stream or a better way to filter this? Thanks.
Could have just done this via a for loop without Streams but trying to see how
I could achieve this in a Stream implementation thus not looking for a for loop solution. Please advice. Thanks.
private NewObject get() {
Map<String, Object> data = // data filled in by an external rest call;
return Optional.ofNullable(data)
// using flatmap here throws above error
.map(Map::entrySet)
.map(entries -> entries.stream()
.filter(entry -> entry.getKey().contains("id100))
// I wish to carry on further operations from here by using the filtered map.
// having issues cos capturedData is a Stream
// even if using flatmap at this stage, capturedData is still a Stream.
// wanting to do the following but can't due to it being a Stream and not a map
).map(capturedData -> {
Map<String, Object> at = (Map<String, Object>) capturedData;
NewObject newObject = new NewObject();
newObject.setName((String) at.get("val1"));
return newObject;
}).orElse(null);
}
Use map to construct the NewObject and use findFirst to get the first value (as per your comment, there will be only one entry whose key has substring id100). Finally use flatMap to unwrap the Optional<NewObject>.
return Optional.ofNullable(data)
.map(Map::entrySet)
.flatMap(entries -> entries.stream()
.filter(entry -> entry.getKey().contains("id100"))
.map(entry -> {
NewObject newObject = new NewObject();
Map<String, String> nestedMap = (Map<String, String>) entry.getValue();
newObject.setName(nestedMap.get("val1"));
return newObject;
})
.findFirst())
.orElse(null);
This code below filters the entryset in data, collects it to a set before performing the next set of operations. findFirst is used so that there is only ever a single entry to deal with.
Optional.ofNullable(data)
.map(Map::entrySet)
.map(entries ->
entries
.stream()
.filter(e -> e.getKey().contains("id1000")).collect(Collectors.toSet()))
.stream()
.findFirst()
.map(capturedData -> {
Map<String, Object> map = (Map<String, Object>) capturedData;
NewObject newObject = new NewObject();
newObject.setName((String) at.get("val1"));
return newObject;
})
.orElse(null);
I have a utility method defined as below.
public static Map<String, Map<String, String>> convertRawMapToStringValues(Map<String, Map<String, Object>> cassandraRowsRawMap) {
Map<String, Map<String, String>> cassandraStrValuesMap = cassandraRowsRawMap.entrySet()
.stream()
.collect(Collectors.toMap(s -> s.getKey(),
s -> s.getValue().entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(),
e -> String.valueOf(e.getValue())))));
return cassandraStrValuesMap;
}
The String.valueOf(e.getValue()) returns a "null" value from the call. I would like to get the null value for the string.
When I tried the below code, I get an NPE on first .collect call.
Map<String, Map<String, String>> cassandraStrValuesMap = cassandraRowsRawMap.entrySet()
.stream()
.collect(Collectors.toMap(s -> s.getKey(),
s -> s.getValue().entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey(),
e -> e.getValue() == null ? null : String.valueOf(e.getValue())))));
return cassandraStrValuesMap;
}
The toMap collector doesn’t support null values. But it doesn’t always have to be the Stream API:
public static <K,T,R> Map<K,R> changeValues(
Map<? extends K, T> in, Function<? super T, ? extends R> f) {
Map<K,R> result = new HashMap<>(in.size());
in.forEach((k,t) -> result.put(k, f.apply(t)));
return result;
}
public static Map<String, Map<String, String>> convertRawMapToStringValues(
Map<String, Map<String, Object>> in) {
return changeValues(in, inner -> changeValues(inner, v -> v==null? null: v.toString()));
}
The utility method returns a map with the same keys and transformed values and is flexible enough to allow a recursive application to do the inner map transformation.
Alternatively, we may adapt the solution of this answer to
public static Map<String, Map<String, String>> convertRawMapToStringValues(
Map<String, Map<String, Object>> in) {
return in.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue().entrySet().stream()
.collect(
HashMap::new,
(m,e2) -> m.put(e2.getKey(),
e2.getValue() == null? null: e2.getValue().toString()),
Map::putAll)));
}
Unlike the original toMap collector, this won’t throw on duplicate keys, but for this specific case where the input is already a Map, there shouldn’t be duplicate keys anyway.
For the sake of this example, let's assume I have a simple type Tuple with two attributes:
interface Tuple<T, U> {
T getFirst();
U getSecond();
}
Now I want to transform a collection of (first, second) tuples into a map which maps each first value to a set of all second values contained in tuples with that specific first value. The method groupSecondByFirst() shows a possible implementation doing what I want:
<T, U> Map<T, Set<U>> groupSecondByFirst(Set<Tuple<T, U>> tuples) {
Map<T, Set<U>> result = new HashMap<>();
for (Tuple<T, U> i : tuples) {
result.computeIfAbsent(i.getFirst(), x -> new HashSet<>()).add(i.getSecond());
}
return result;
}
If the input was [(1, "one"), (1, "eins"), (1, "uno"), (2, "two"), (3, "three")] the output would be { 1 = ["one", "eins", "uno"], 2 = ["two"], 3 = ["three"] }
I would like to know whether and how I can implement this using the streams framework. The best I got is the following expression, which returns a map which contains the full tuple as values and not just their second elements:
Map<T, Set<Tuple<T, U>>> collect = tuples.stream().collect(
Collectors.groupingBy(Tuple::getFirst, Collectors.toSet()));
I found a solution; It involves Collections.mapping(), which can wrap a collector and apply mapping function over stream to supply elements to the wrapped collector:
static <T, U> Map<T, Set<U>> groupSecondByFirst(Collection<Tuple<T, U>> tuples) {
return tuples
.stream()
.collect(
Collectors.groupingBy(
Tuple::getFirst,
Collectors.mapping(
Tuple::getSecond,
Collectors.toSet())));
}
I'm trying to perform a map operation on each entry in a Map object.
I need to take a prefix off the key and convert the value from one type to another. My code is taking configuration entries from a Map<String, String> and converting to a Map<String, AttributeType> (AttributeType is just a class holding some information. Further explanation is not relevant for this question.)
The best I have been able to come up with using the Java 8 Streams is the following:
private Map<String, AttributeType> mapConfig(Map<String, String> input, String prefix) {
int subLength = prefix.length();
return input.entrySet().stream().flatMap((Map.Entry<String, Object> e) -> {
HashMap<String, AttributeType> r = new HashMap<>();
r.put(e.getKey().substring(subLength), AttributeType.GetByName(e.getValue()));
return r.entrySet().stream();
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
Being unable to construct an Map.Entry due to it being an interface causes the creation of the single entry Map instance and the use of flatMap(), which seems ugly.
Is there a better alternative? It seems nicer to do this using a for loop:
private Map<String, AttributeType> mapConfig(Map<String, String> input, String prefix) {
Map<String, AttributeType> result = new HashMap<>();
int subLength = prefix.length();
for(Map.Entry<String, String> entry : input.entrySet()) {
result.put(entry.getKey().substring(subLength), AttributeType.GetByName( entry.getValue()));
}
return result;
}
Should I avoid the Stream API for this? Or is there a nicer way I have missed?
Simply translating the "old for loop way" into streams:
private Map<String, String> mapConfig(Map<String, Integer> input, String prefix) {
int subLength = prefix.length();
return input.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey().substring(subLength),
entry -> AttributeType.GetByName(entry.getValue())));
}
Please make the following part of the Collectors API:
<K, V> Collector<? super Map.Entry<K, V>, ?, Map<K, V>> toMap() {
return Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue);
}
Question might be a little dated, but you could simply use AbstractMap.SimpleEntry<> as follows:
private Map<String, AttributeType> mapConfig(
Map<String, String> input, String prefix) {
int subLength = prefix.length();
return input.entrySet()
.stream()
.map(e -> new AbstractMap.SimpleEntry<>(
e.getKey().substring(subLength),
AttributeType.GetByName(e.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
any other Pair-like value object would work too (ie. ApacheCommons Pair tuple).
On Java 9 or later, Map.entry can be used, so long as you know that neither the key nor value will be null. If either value could legitimately be null, AbstractMap.SimpleEntry (as suggested in another answer) or AbstractMap.SimpleImmutableEntry are good alternatives.
private Map<String, AttributeType> mapConfig(Map<String, String> input, String prefix) {
int subLength = prefix.length();
return input.entrySet().stream().map(e ->
Map.entry(e.getKey().substring(subLength), AttributeType.GetByName(e.getValue()))
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
That being said, in this particular case, there's no real value in the interim Entry object, and it would be more idiomatic to perform the key/value mapping within Collectors.toMap (as demonstrated in this other answer). However, there are legitimate reasons to create interim entry objects, so it's still helpful to know.
As an alternative to using the built-in Java stream support, the StreamEx library could be used. It has fluent support for streams of Entry objects by way of the EntryStream class:
private Map<String, String> mapConfig(Map<String, Integer> input, String prefix) {
int subLength = prefix.length();
return EntryStream.of(input)
.mapKeys(key -> key.substring(subLength))
.mapValues(AttributeType::GetByName)
.toMap();
}
Here is a shorter solution by abacus-common
Stream.of(input).toMap(e -> e.getKey().substring(subLength),
e -> AttributeType.GetByName(e.getValue()));
I have a Map<String, String> the String key is nothing but numeric value like "123" etc. I'm getting numeric value because this values are coming from the UI in my JSF component. I don't want to change the contract of UI component.
Now I would like to create a Map<Long, String> based on the above Map, I saw some transform methods in the Maps class but all are focusing on the converting value and not key.
Is there any better way to convert Map<String, String> to Map<Long, String> ?
#Vivin's answer is correct, but I think it's useful to explain why Guava doesn't have any method to allow you to transform the keys of a Map (or to transform a Set at all).
All of Guava's methods for transforming and filtering produce lazy results... the function/predicate is only applied when needed as the object is used. They don't create copies. Because of that, though, a transformation can easily break the requirements of a Set.
Let's say, for example, you have a Map<String, String> that contains both "1" and "01" as keys. They are both distinct Strings, and so the Map can legally contain both as keys. If you transform them using Long.valueOf(String), though, they both map to the value 1. They are no longer distinct keys. This isn't going to break anything if you create a copy of the map and add the entries, because any duplicate keys will overwrite the previous entry for that key. A lazily transformed Map, though, would have no way of enforcing unique keys and would therefore break the contract of a Map.
UPDATE for Java 8
You can use streams to do this:
Map<Long, String> newMap = oldMap.entrySet().stream()
.collect(Collectors.toMap(e -> Long.parseLong(e.getKey()), Map.Entry::getValue));
This assumes that all keys are valid string-representations of Longs. Also, you can have collisions when transforming; for example, "0" and "00" both map to 0L.
I would think that you'd have to iterate over the map:
Map<Long, String> newMap = new HashMap<Long, String>();
for(Map.Entry<String, String> entry : map.entrySet()) {
newMap.put(Long.parseLong(entry.getKey()), entry.getValue());
}
This code assumes that you've sanitized all the values in map (so no invalid long values).
I'm hoping there is a better solution.
EDIT
I came across the CollectionUtils#transformedCollection(Collection, Transformer) method in Commons Collection-Utils that looks like it might do what you want. Scratch that, it only works for classes that implement Collection.
You can now use Java 8 stream, map, collect to do this in a more readable, clean manner.
Map<String, String> oldMap
Map<Long, String> newMap = oldMap.entrySet().stream()
.collect(Collectors.toMap(entry -> Long.parseLong(entry.getKey()), Map.Entry::getValue));
Here's an updated version of one of the answers below to make the resulting Map unmodifiable (no, it's not using Guava, just plain Java 8):
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toMap;
...
newMap = oldMap.entrySet().stream().collect(collectingAndThen(
toMap((Map.Entry<String, String> entry) -> transformKey(entry.getKey()),
(Map.Entry<String, String> entry) -> transformValue(entry.getValue())),
Collections::unmodifiableMap)));
The short answer is no, Guava does not provide this one out of the box.
The simple way would be something like below. There are some cautions, however.
public static <K, V, L, W> Map<L, W> transformMap(Map<K, V> map, Function<K, L> keyFunction, Function<V, W> valueFunction) {
Map<L, W> transformedMap = newHashMap();
for (Entry<K, V> entry : map.entrySet()) {
transformedMap.put(
keyFunction.apply(entry.getKey()),
valueFunction.apply(entry.getValue()));
}
return transformedMap;
}
public static <K, V, L> Map<L, V> transformKeys(Map<K, V> map, Function<K, L> keyFunction) {
return transformMap(map, keyFunction, Functions.<V>identity());
}
Guava's transformers are all "lazy" or view-based. I think to implement a map key transformer, you'd want a two-way function. My understanding is that there is a Converter that is in the works by the Guava team, which would solve that problem.
The other problem you run into is that you'd have to deal with the possibility of duplicates in order to be "Jimmy-proof", another Guava principle. One way to handle that would be to return a Multimap; another would be to throw an exception when you encounter duplicates. What I would not suggest is hiding the problem e.g. by ignoring subsequent entries with duplicate keys, or by overwriting the new entry with the duplicate key.
Answer using Java 8 and jOOλ
private static <K1, K2, V> Map<K2, V> mapKeys(Map<K1, V> map, Function<K1, K2> keyMapper) {
return Seq.seq(map).map(t -> t.map1(keyMapper)).toMap(Tuple2::v1, Tuple2::v2);
}