Partially Flatten Nested Collections in Java using Lambdas - java

I have a nested collection as such
Map<Integer, Map<Integer, List<Integer>>> nodes = new TreeMap<>()
I need to convert the inner map into a List<List<Integer>>. The order of the inner list has to be preserved. Essentially for each entry in the outer map, iterate through the inner map, add the List as is to the List of Lists.
I can do it the old fashioned way.
List<List<Integer>> result = new ArrayList<>();
for(Map.Entry<Integer, TreeMap<Integer, List<Integer>>> entry : nodes.entrySet()) {
Map<Integer, List<Integer>> outer = entry.getValue();
ArrayList<Integer> tmp = new ArrayList<>();
for (Map.Entry<Integer, List<Integer>> inner : outer.entrySet()) {
tmp.addAll(inner.getValue());
}
result.add(tmp);
}
How do to this with lambdas? This doesn't work
nodes.entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()).map(e2 -> result.add(e2.getValue()))

How do to this with lambdas? This doesn't work
Here you never invoke a termination operation, so the stream is never consumed. :
nodes.entrySet().stream().flatMap(e -> e.getValue().entrySet().stream()).map(e2 -> result.add(e2.getValue()))
Add any terminal operation such as count() and you could see the stream operated.
Don't forget that Streams are lazy and so the computation is effectively performed only when the terminal operation is invoked.
So you guess that your way is not the right way to do things with Stream.
You don't need to use the List as a variable that you will populate in the stream. Streams are designed to collect as they produce a result and the collect to a List is finally the terminal operation that missed in your initial code.
Besides as a side note you should just stream the values of each Map level instead of the entries since you never use the keys.
Here the code with for each step the actual return type :
List<List<Integer>> result =
nodes.values() // Collection<Map<Integer, List<Integer>>>
.stream() // Stream<Map<Integer, List<Integer>>>
.flatMap(m -> m.values() // Collection<List<Integer>>>
.stream()) // Stream<List<Integer>>>
// flatMap() prevents Stream<Stream<...>>.
// Indeed we get just Stream<List...>>
.collect(Collectors.toList());

This one should do the trick:
Map<Integer, Map<Integer, List<Integer>>> nodes = new TreeMap<>();
List<List<Integer>> list = nodes.values()
.stream()
.flatMap( map -> map.values().stream() )
.collect( Collectors.toList() );
Explanation:
First you get stream of maps from map by using:
nodes.values().stream()
then you flatten those maps with:
.flatMap( map -> map.values().stream() )
And finally collect them with:
.collect( Collectors.toList() )

Related

Map objects by occurrences in list of pairs

I need to map a list of pairs of objects into <ocurrences, list of Objs with those ocurrences>, I've tried using streams directly on the input list of pairs but I'm still kind of new to java and couldn't figure it out, so I was trying to do something like this, but it's probably not close to the best way to do it.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
Map<Integer,ArrayList<Obj>> nBorders = new HashMap<>();
List<Obj> list = new ArrayList<>();
for(Pair<Obj, Obj> pair : lf) {
list.add(pair.getKey());
list.add(pair.getValue());
}
nBorders = list.stream().collect(Collectors.groupingBy(...);
return nBorders;
}
so for example, for lf = {(o1,o2),(o3,o2),(o5,o4),(o4,o1),(o3,o4),(o7,o1),(o5,o8),(o3,o10),(o4,o5),(o3,o7),(o9,o8)} the result should be {(1,{o9,o10}),(2,{o2,o7,o8,}),(3,{o1,o5}),(4,{o3,o4})}.
I'm really confused on how to do this, if someone could help, I'd appreciate it, thanks.
This can be done this way:
create a stream from the pairs to concatenate first/second values using Stream::flatMap
count the occurrences - build an intermediate map <Obj, Integer> using Collectors.groupingBy + Collectors.summingInt (to keep integer)
create an inverse map <Integer, List> from the stream of the entries in the intermediate map using Collectors.groupingBy + Collectors.mapping
Optionally, if an order in the resulting map is critical, a LinkedHashMap may be created from the entries of the intermediate frequency map sorted by value.
public Map<Integer,ArrayList<Obj>> numBorders(List<Pair<Obj,Obj>> lf) {
return lf.stream() // Stream<Pair>
.flatMap(p -> Stream.of(p.getKey(), p.getValue())) // Stream<Obj>
.collect(Collectors.groupingBy(
obj -> obj,
Collectors.summingInt(obj -> 1)
)) // Map<Obj, Integer>
.entrySet()
.stream() // Stream<Map.Entry<Obj, Integer>>
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.groupingBy(
Map.Entry::getValue, // frequency is key
LinkedHashMap::new,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
)); // Map<Integer, List<Obj>>
}

How to create a reverse map when original map contains collection as the value?

Let's say my original Map contains the following:
Map<String, Set<String>> original = Maps.newHashMap();
original.put("Scott", Sets.newHashSet("Apple", "Pear", "Banana");
original.put("Jack", Sets.newHashSet("Banana", "Apple", "Orange");
And I want to create a reversed Map containing the following:
"Apple": ["Scott", "Jack"]
"Pear": ["Scott"]
"Banana": ["Scott", "Jack"]
"Orange": ["Jack"]
I know it can be done in old fashion (pre-Java 8), but how do I achieve the same using Java Stream API?
Map<String, Set<String>> reversed = original.entrySet().stream().map(x -> ????).collect(??)
There's similar question posted here, but that only works for single valued Maps.
You can break the Map into key-value pairs (where each key and value is a single String) by using flatMap, and then collect them as you wish:
Map<String,Set<String>> rev =
original.entrySet ()
.stream ()
.flatMap (e -> e.getValue ()
.stream ()
.map (v -> new SimpleEntry<String,String>(v,e.getKey ())))
.collect(Collectors.groupingBy (Map.Entry::getKey,
Collectors.mapping (Map.Entry::getValue,
Collectors.toSet())));
System.out.println (rev);
Output:
{Apple=[Jack, Scott], Pear=[Scott], Orange=[Jack], Banana=[Jack, Scott]}
A more imperative but simpler solution would be using forEach :
Map<String, Set<String>> original,result; // initialised
original.forEach((key, value) -> value.forEach(v ->
result.computeIfAbsent(v, k -> new HashSet<>()).add(key)));

Convert Map<String, Object> to Map<String, Set<Object>> with filter and streams

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.

Java 8 -Flatten Map with Lists

I have a structure such as Map<String,List<Map<String,Object>>. I want to apply a function to the map as follows. The method takes a key and uses a
Map<String,Object> of the list. Each key has several Map<String,Object> in the list. How can I apply the process method to the map's key for each value of Map<String,Object>? I was able to use to forEach loops(see below) but I have a feeling this is not the best way to solve the problem in a functional way.
TypeProcessor p=new TypeProcessor.instance();
//Apply this function to the key and each map from the list
// The collect the Result returned in a list.
Result process(String key, Map<String,Object> dataPoints);
List<Result> list = new ArrayList<>();
map.forEach(key,value) -> {
value.forEach(innerVal -> {
Result r=p.process(key,innerVal);
list.add(r):
});
});
It seems from your code that you want to apply process for the entire Map, so you could do it like this:
List<Result> l = map.entrySet()
.stream()
.flatMap(e -> e.getValue().stream().map(value -> process(e.getKey(), value)))
.collect(Collectors.toList());
Well, assuming map contains key, you don't need any foreach. Just obtain the value from the outer map, stream it, map to your new object and collect to a List:
List<Result> list =
map.get(key)
.stream()
.map(v -> p.process(key,v))
.collect(Collectors.toList());

Java 8 stream Map<Long, List<MyClass>> to Map<Long, Set<Long>>

I would like to convert (using Java 8 stream) a Map<Long, List<MyClass>> in a Map<Long, Set<Long>> where the Set<Long> represents the id of each MyClass of the List.
I have tried:
myFirstMap.entrySet().stream()
.map(e -> e.getValue().stream()
.map(MyClass::getId)
.collect(Collectors.toSet()))
But I cannot see how to get the result.
You are mapping instances of Map.Entry to Set<Long> instances which implies loosing track of the original map’s keys which makes it impossible to collect them into a new map having the same keys.
The first option is to map the Map.Entry<Long, List<MyClass>> instances to Map.Entry<Long, Set<Long>> instances and then collect the entries to a new map:
Map<Long, Set<Long>> result=
myFirstMap.entrySet().stream()
.map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(),
e.getValue().stream().map(MyClass::getId).collect(Collectors.toSet())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
The alternative is to fuse the map and collect step into one, to do the transformation right in the value function provided to the toMap collector:
Map<Long, Set<Long>> result=
myFirstMap.entrySet().stream().collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().stream().map(MyClass::getId).collect(Collectors.toSet())));
This way, you avoid creating new Map.Entry instances and get more concise code, however, loose flexibility as you can’t chain additional stream operations in-between.
Another solution, without the overhead of the outer Stream is to use Map.forEach() like this:
Map<Long,Set<Long>> result = new HashMap<>();
myFirstMap.forEach((k,v) ->
result.put(k, v.stream()
.map(MyClass::getId)
.collect(Collectors.toSet())));
Which is really just a convenience method to do this:
Map<Long,Set<Long>> result = new HashMap<>();
for (Map.Entry<Long, List<MyClass>> entry : myFirstMap.entrySet()) {
result.put(entry.getKey(), entry.getValue().stream()
.map(MyClass::getId)
.collect(Collectors.toSet()));
}

Categories

Resources