I want to use a Java Stream to run over a List of POJOs, such as the list List<A> below, and transform it into a Map Map<String, Set<String>>.
For example, class A is:
class A {
public String name;
public String property;
}
I wrote the code below that collects the values into a map Map<String, String>:
final List<A> as = new ArrayList<>();
// the list as is populated ...
// works if there are no duplicates for name
final Map<String, String> m = as.stream().collect(Collectors.toMap(x -> x.name, x -> x.property));
However, because there might be multiple POJOs with the same name, I want the value of the map be a Set. All property Strings for the same key name should go into the same set.
How can this be done?
// how do i create a stream such that all properties of the same name get into a set under the key name
final Map<String, Set<String>> m = ???
groupingBy does exactly what you want:
import static java.util.stream.Collectors.*;
...
as.stream().collect(groupingBy((x) -> x.name, mapping((x) -> x.property, toSet())));
#Nevay 's answer is definitely the right way to go by using groupingBy, but it is also achievable by toMap by adding a mergeFunction as the third parameter:
as.stream().collect(Collectors.toMap(x -> x.name,
x -> new HashSet<>(Arrays.asList(x.property)),
(x,y)->{x.addAll(y);return x;} ));
This code maps the array to a Map with a key as x.name and a value as HashSet with one value as x.property. When there is duplicate key/value, the third parameter merger function is then called to merge the two HashSet.
PS. If you use Apache Common library, you can also use their SetUtils::union as the merger
Same Same But Different
Map<String, Set<String>> m = new HashMap<>();
as.forEach(a -> {
m.computeIfAbsent(a.name, v -> new HashSet<>())
.add(a.property);
});
Also, you can use the merger function option of the Collectors.toMap function
Collectors.toMap(keyMapper,valueMapper,mergeFunction) as follows:
final Map<String, String> m = as.stream()
.collect(Collectors.toMap(
x -> x.name,
x -> x.property,
(property1, property2) -> property1+";"+property2);
Related
I'm learning Java 8 - Java 11 and I got a code that I'm converting to java-streams. I have the following classes:
class Resource {
List<Capability> capabilities;
}
class Capability {
String namespace;
Map<String, Object> attributes;
}
I have a stream of Resources and I want to extract all its capabilities attributes from two different namespaces ("a", "b") to a Map<Resource, Map<String, Object>> that I have sure that do not have duplicates keys.
I did many attempts using map, flatMap but with those, I can't keep a reference of the main resource object. Using the new feature of java9 I could progress, but I'm stuck on the code below where I was able to return all attributes, but in a set.
I was not able yet to filter by a capability namespace and also put them in a map:
Map<Resource, Set<Object>> result = pResolved.stream()
.collect(groupingBy(t -> t, flatMapping(
resource -> resource.getCapabilities(null).stream(),
flatMapping(
cap -> cap.getAttributes().entrySet().stream(),
toSet()))));
Seems that I'm on the right path.
There is a way using only java-8 methods as well:
Map<String, Set<Object>> result = pResolved.stream()
.map(Resource::getCapabilities) // Stream<List<Capability>>
.flatMap(List::stream) // Stream<Capability>
.collect(Collectors.toMap( // Map<String, Set<Object>>
c -> c.getNamespace(), // Key: String (namespace)
i -> new HashSet<>(i.getAttributes().values()))); // Value: Set of Map values
Let's assume the sample input is:
Resource [capabilities=[
Capability [namespace=a, attributes={a1=aa1, a2=aa2, a3=aa3}]]]
Resource [capabilities=[
Capability [namespace=b, attributes={b2=bb2, b3=bb3, b1=bb1}],
Capability [namespace=c, attributes={c3=cc3, c1=cc1, c2=cc2}]]]
Then the code above would result in:
a: [aa1, aa3, aa2]
b: [bb1, bb3, bb2]
c: [cc1, cc3, cc2]
You could instead use Collectors.toMap as the downstream :
Map<Resource, Map<String, Object>> result = pResolved
.stream()
.collect(groupingBy(Function.identity(),
flatMapping(resource -> resource.getCapabilities().stream(),
flatMapping(cap -> cap.getAttributes().entrySet().stream(),
toMap(Map.Entry::getKey, Map.Entry::getValue)))));
I would like to convert my map which looks like this:
{
key="someKey1", value=Apple(id="1", color="green"),
key="someKey2", value=Apple(id="2", color="red"),
key="someKey3", value=Apple(id="3", color="green"),
key="someKey4", value=Apple(id="4", color="red"),
}
to another map which puts all apples of the same color into the same list:
{
key="red", value=list={apple1, apple3},
key="green", value=list={apple2, apple4},
}
I tried the following:
Map<String, Set<Apple>> sortedApples = appleMap.entrySet()
.stream()
.collect(Collectors.toMap(l -> l.getColour, ???));
Am I on the right track? Should I use filters for this task? Is there an easier way?
Collectors.groupingBy is more suitable than Collectors.toMap for this task (though both can be used).
Map<String, List<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour));
Or, to group them into Sets use:
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour,
Collectors.mapping(Function.identity(),
Collectors.toSet())));
or (as Aomine commented):
Map<String, Set<Apple>> sortedApples =
appleMap.values()
.stream()
.collect(Collectors.groupingBy(Apple::getColour, Collectors.toSet()));
if you want to proceed with toMap you can get the result as follows:
map.values() // get the apples
.stream() // Stream<Apple>
.collect(toMap(Apple::getColour, // group by colour
v -> new HashSet<>(singleton(v)), // have values as set of apples
(l, r) -> {l.addAll(r); return l;})); // merge colliding apples by colour
stream over the map values instead of entrySet because we're not concerned with the map keys.
Apple::getColour is the keyMapper function used to extract the "thing" we wish to group by, in this case, the Apples colour.
v -> new HashSet<>(singleton(v)) is the valueMapper function used for the resulting map values
(l, r) -> {l.addAll(r); return l;} is the merge function used to combine two HashSet's when there is a key collision on the Apple's colour.
finally, the resulting map is a Map<String, Set<Apple>>
but this is better with groupingBy and toSet as downstream:
map.values().stream().collect(groupingBy(Apple::getColour, toSet()));
stream over the map values instead of entrySet because we're not concerned with the map keys.
groups the Apple's by the provided classification function i.e. Apple::getColour and then collect the values in a Set hence the toSet downstream collector.
finally, the resulting map is a Map<String, Set<Apple>>
short, readable and the idiomatic approach.
You could also do it without a stream:
Map<String, Set<Apple>> res = new HashMap<>();
map.values().forEach(a -> res.computeIfAbsent(a.getColour(), e -> new HashSet<>()).add(a));
iterate over the map values instead of entrySet because we're not concerned with the map keys.
if the specified key a.getColour() is not already associated with a value, attempts to compute its value using the given mapping function e -> new HashSet<>() and enters it into the map. we then add the Apple to the resulting set.
if the specified key a.getColour() is already associated with a value computeIfAbsent returns the existing value associated with it and then we call add(a) on the HashSet to enter the Apple into the set.
finally, the resulting map is a Map<String, Set<Apple>>
You can use Collectors.groupingBy and Collectors.toSet()
Map<String, Set<Apple>> sortedApples = appleMap.values() // Collection<Apple>
.stream() // Stream<Apple>
.collect(Collectors.groupingBy(Apple::getColour, // groupBy colour
Collectors.mapping(a -> a, Collectors.toSet()))); // collect to Set
You've asked how to do it with streams, yet here's another way:
Map<String, Set<Apple>> result = new LinkedHashMap<>();
appleMap.values().forEach(apple ->
result.computeIfAbsent(apple.getColor(), k -> new LinkedHashSet<>()).add(apple));
This uses Map.computeIfAbsent, which either returns the set mapped to that color or puts an empty LinkedHashSet into the map if there's nothing mapped to that color yet, then adds the apple to the set.
EDIT: I'm using LinkedHashMap and LinkedHashSet to preserve insertion order, but could have used HashMap and HashSet, respectively.
I have two maps with the following data type,
Map<Pair<Long,String>, List<String>> stringValues;
Map<Pair<Long,String>, List<Boolean>> booleanValues ;
I want to merge the above maps to the following datastructure
Map<Pair<Long,String>, Pair<List<String>,List<Boolean>>> stringBoolValues;
My input has two maps with same key but different values. I want to group them to a pair. Can I use java stream to achieve this ?
other simple way is like this:
stringValues.forEach((key, value) -> {
Pair<List<String>, List<Boolean>> pair = new Pair<>(value, booleanValues.get(key));
stringBoolValues.put(key, pair);
});
stringBoolValues = stringValues
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> new Pair<>(entry.getValue(), booleanValues.get(entry.getKey()))));
Try like this:
Set<Pair<Long,String>> keys = new HashSet<>(stringValues.keySet());
keys.addAll(booleanValues.keySet());
keys.stream().collect(Collectors.toMap(key -> key,
key -> new Pair<>(stringValues.get(key), booleanValues.get(key))));
Precondition: You had overridden equals()/hashCode() properly for Pair<Long, String>
Map<Pair<Long,String>, Pair<List<String>,List<Boolean>>> stringBoolValues
= Stream.of(stringValues.keySet(),booleanValues.keySet())
.flatMap(Set::stream)
.map(k -> new SimpleEntry<>(k, Pair.of(stringValues.get(k), booleanValues.get(k)))
.collect(toMap(Entry::getKey, Entry::getValue));
Where Pair.of is:
public static Pair<List<String>,List<Boolean>> of(List<String> strs, List<Boolean> bls) {
List<String> left = Optional.ofNullable(strs).orElseGet(ArrayList::new);
List<Boolean> right = Optional.ofNullable(bls).orElseGet(ArrayList::new);
return new Pair<>(left, right);
}
You can even use Map.computeIfAbsent to avoid the need of explicit checking for null.
Just using the valueMapper in Collectors.toMap to merge values in two different maps easily:
Map<Pair<Long, String>, Pair<List<String>, List<Boolean>>> stringBoolValues = stringValues.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> new Pair(entry.getValue(), booleanValues.get(entry.getKey()))));
I'd like to convert a Map <String, Integer> from List<String> in java 8 something like this:
Map<String, Integer> namesMap = names.stream().collect(Collectors.toMap(name -> name, 0));
because I have a list of Strings, and I'd like to to create a Map, where the key is the string of the list, and the value is Integer (a zero).
My goal is, to counting the elements of String list (later in my code).
I know it is easy to convert it, in the "old" way;
Map<String,Integer> namesMap = new HasMap<>();
for(String str: names) {
map1.put(str, 0);
}
but I'm wondering there is a Java 8 solution as well.
As already noted, the parameters to Collectors.toMap have to be functions, so you have to change 0 to name -> 0 (you can use any other parameter name instead of name).
Note, however, that this will fail if there are duplicates in names, as that will result in duplicate keys in the resulting map. To fix this, you could pipe the stream through Stream.distinct first:
Map<String, Integer> namesMap = names.stream().distinct()
.collect(Collectors.toMap(s -> s, s -> 0));
Or don't initialize those defaults at all, and use getOrDefault or computeIfAbsent instead:
int x = namesMap.getOrDefault(someName, 0);
int y = namesMap.computeIfAbsent(someName, s -> 0);
Or, if you want to get the counts of the names, you can just use Collectors.groupingBy and Collectors.counting:
Map<String, Long> counts = names.stream().collect(
Collectors.groupingBy(s -> s, Collectors.counting()));
the toMap collector receives two mappers - one for the key and one for the value. The key mapper could just return the value from the list (i.e., either name -> name like you currently have, or just use the builtin Function.Identity). The value mapper should just return the hard-coded value of 0 for any key:
namesMap =
names.stream().collect(Collectors.toMap(Function.identity(), name -> 0));
I have this Map:
Map<Integer, Set<String>> map = ...
And I have this class Foo:
class Foo {
int id;
String name;
}
I want to convert the map to List<Foo>. Is there a convenient manner in Java 8 to do this?
Currently, my way is:
List<Foo> list = new ArrayList<>((int) map.values().flatMap(e->e.stream()).count()));
for(Integer id : map.keySet()){
for(String name : map.get(id)){
Foo foo = new Foo(id,name);
list.add(foo);
}
}
I feel it's too cumbersome.
You can have the following:
List<Foo> list = map.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(name -> new Foo(e.getKey(), name)))
.collect(toList());
For each entry of the map, we create a Stream of the value and map it to the corresponding Foo and then flatten it using flatMap.
The main reason for your version being cumbersome is that you have decided to calculate the capacity of the ArrayList first. Since this calculation requires iterating over the entire map, there is unlikely to be any benefit in this. You definitely should not do such a thing unless you have proved using a proper benchmark that it is needed.
I can't see anything wrong with just using your original version but with the parameterless constructor for ArrayList instead. If you also get rid of the redundant local variable foo, it's actually fewer characters (and clearer to read) than the stream version.
final Map<Integer, Set<String>> map = new HashMap<>();
map
.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(s -> new Foo(e.getKey(), s)))
.collect(Collectors.toList());