Using streams to group Map attributes from inner objects? - java

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

Related

Filter operation via Stream and maintain as map instead of Stream

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

How to convert a csv into HashMap with java streams without creating entry in intermediate operation?

I'm writing a junit test where I want to import the expected result from a csv file, as a HashMap.
The following works, but I find it kind boilerplate that I first create a MapEntry.entry(), which I than collect into a new HashMap.
csv:
#key;amount
key1;val1
key2;val2
...
keyN;valN
test:
Map<String, BigDecimal> expected = Files.readAllLines(
Paths.get("test.csv"))
.stream()
.map(line -> MapEntry.entry(line.split(",")[0], line.split(",")[1]))
.collect(Collectors.toMap(Map.Entry::getKey, item -> new BigDecimal(item.getValue())));
Especially I'm looking for a oneliner solution like this. I mean: can I prevent having to create a MapEntry.entry explicit before again collecting it to a hashmap?
Can this be done better? Or is there even any junit utility that can already read a csv?
You don't need to create entry, you can split the line into array using map function and then use Collectors.toMap
Map<String, BigDecimal> expected = Files.readAllLines(
Paths.get("test.csv"))
.stream()
.map(line->line.split(","))
.filter(line->line.length>1)
.collect(Collectors.toMap(key->key[0], value -> new BigDecimal(value[1])));
If you want to collect entries into a specific type you can use overloaded Collectors.toMap with mapSupplier
Returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements.
HashMap<String, BigDecimal> expected = Files.readAllLines(
Paths.get("test.csv"))
.stream()
.map(line->line.split(","))
.filter(line->line.length>1)
.collect(Collectors.toMap(key->key[0], value -> new BigDecimal(value[1]),(val1,val2)->val1, HashMap::new));
}
This worked for me:
Map<String, BigDecimal> result = Files.readAllLines(Paths.get("test.csv"))
.stream()
.collect(Collectors.toMap(l -> l.split(",")[0], l -> new BigDecimal(l.split(",")[1])));

Groupby counts in java

I am pretty new to java moving from c#. I have the following class.
class Resource {
String name;
String category;
String component;
String group;
}
I want to know the following numbers:
1. Count of resources in the category.
2. Distinct count of components in each category. (component names can be duplicate)
3. Count of resources grouped by category and group.
I was able to achieve a little bit of success using Collectors.groupingBy. However, the result is always like this.
Map<String, List<Resource>>
To get the counts I have to parse the keyset and compute the sizes.
Using c# linq, I can easily compute all the above metrics.
I am assuming there is definitely a better way to do this in java as well. Please advise.
For #1, I'd use Collectors.groupingBy along with Collectors.counting:
Map<String, Long> resourcesByCategoryCount = resources.stream()
.collect(Collectors.groupingBy(
Resource::getCategory,
Collectors.counting()));
This groups Resource elements by category, counting how many of them belong to each category.
For #2, I wouldn't use streams. Instead, I'd use the Map.computeIfAbsent operation (introduced in Java 8):
Map<String, Set<String>> distinctComponentsByCategory = new LinkedHashMap<>();
resources.forEach(r -> distinctComponentsByCategory.computeIfAbsent(
r.getCategory(),
k -> new HashSet<>())
.add(r.getGroup()));
This first creates a LinkedHashMap (which preserves insertion order). Then, Resource elements are iterated and put into this map in such a way that they are grouped by category and each group is added to a HashSet that is mapped to each category. As sets don't allow duplicates, there won't be duplicated groups for any category. Then, the distinct count of groups is the size of each set.
For #3, I'd again use Collectors.groupingBy along with Collectors.counting, but I'd use a composite key to group by:
Map<List<String>, Long> resourcesByCategoryAndGroup = resources.stream()
.collect(Collectors.groupingBy(
r -> Arrays.asList(r.getCategory(), r.getGroup()), // or List.of
Collectors.counting()));
This groups Resource elements by category and group, counting how many of them belong to each (category, group) pair. For the grouping key, a two-element List<String> is being used, with the category being its 1st element and the component being its 2nd element.
Or, instead of using a composite key, you could use nested grouping:
Map<String, Map<String, Long>> resourcesByCategoryAndGroup = resources.stream()
.collect(Collectors.groupingBy(
Resource::getCategory,
Collectors.groupingBy(
Resource::getGroup,
Collectors.counting())));
Thanks Fedrico for detailed response. #1 and #3 worked great. For #2, i would like to see an output of Map. Here's the code that i am using currently to get that count. This is without using collectors in old style.
HashMap<String, HashSet<String>> map = new HashMap<>();
for (Resource resource : resources) {
if (map.containsKey(resource.getCategory())) {
map.get(resource.getCategory()).add(resource.getGroup());
} else
HashSet<String> componentSet = new HashSet<>();
componentSet.add(resource.getGroup());
map.put(resource.getCategory(), componentSet);
}
}
log.info("Group count in each category");
for (Map.Entry<String, HashSet<String>> entry : map.entrySet()) {
log.info("{} - {}", entry.getKey(), entry.getValue().size());
}

Java List<E> to Map<P, List<E>> were key is some property of E and value is E with that property

I would like how to convert Java List to Map. Were key in a map is some property of the list element (different elements might have the same property) and value is a list of those list items (having the same property).
eg.List<Owner> --> Map<Item, List<Owner>>. I found a few List to Map questions, but it was not I want to do.
What I came with is:
List<Owner> owners = new ArrayList<>(); // populate from file
Map<Item, List<Owner>> map = new HashMap<>();
owners.parallelStream()
.map(Owner::getPairStream)
.flatMap(Function.identity())
.forEach(pair -> {
map.computeIfPresent(pair.getItem(), (k,v)-> {
v.add(pair.getOwner());
return v;
});
map.computeIfAbsent(pair.getItem(), (k) -> {
List<Owner> list = new ArrayList<>();
list.add(pair.getOwner());
return list;
});
});
PasteBin
I can put forEach part to a separate method, but it still feels too verbose. Plus I made a Pair class just to make it work. I tried to look in to Collectors but couldn't get my head around to do what I wanted.
From where this is, you can simplify your code by using groupingBy:
Map<Item, List<Owner>> map = owners.stream()
.flatMap(Owner::getPairStream)
.collect(Collectors.groupingBy(Pair::getItem,
Collectors.mapping(Pair::getOwner,
Collectors.toList())));
You can also dispense with the Pair class by using SimpleEntry:
Map<Item, List<Owner>> map = owners.stream()
.flatMap(owner -> owner.getItems()
.stream()
.map(item -> new AbstractMap.SimpleEntry<>(item, owner)))
.collect(Collectors.groupingBy(Entry::getKey,
Collectors.mapping(Entry::getValue,
Collectors.toList())));
Note that I'm assuming that Item has equals and hashCode overridden accordingly.
Side notes:
You can use map.merge instead of successively calling map.computeIfPresent and map.computeIfAbsent
HashMap and parallelStream make a bad combination (HashMap isn't thread-safe)

Java Stream Collectors.toMap value is a Set

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

Categories

Resources