Java 8 -Flatten Map with Lists - java

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

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>>
}

Java Map with List value to list using streams?

I am trying to rewrite the method below using streams but I am not sure what the best approach is? If I use flatMap on the values of the entrySet(), I lose the reference to the current key.
private List<String> asList(final Map<String, List<String>> map) {
final List<String> result = new ArrayList<>();
for (final Entry<String, List<String>> entry : map.entrySet()) {
final List<String> values = entry.getValue();
values.forEach(value -> result.add(String.format("%s-%s", entry.getKey(), value)));
}
return result;
}
The best I managed to do is the following:
return map.keySet().stream()
.flatMap(key -> map.get(key).stream()
.map(value -> new AbstractMap.SimpleEntry<>(key, value)))
.map(e -> String.format("%s-%s", e.getKey(), e.getValue()))
.collect(Collectors.toList());
Is there a simpler way without resorting to creating new Entry objects?
A stream is a sequence of values (possibly unordered / parallel). map() is what you use when you want to map a single value in the sequence to some single other value. Say, map "alturkovic" to "ALTURKOVIC". flatMap() is what you use when you want to map a single value in the sequence to 0, 1, or many other values. Hence why a flatMap lambda needs to turn a value into a stream of values. flatMap can thus be used to take, say, a list of lists of string, and turn that into a stream of just strings.
Here, you want to map a single entry from your map (a single key/value pair) into a single element (a string describing it). 1 value to 1 value. That means flatMap is not appropriate. You're looking for just map.
Furthermore, you need both key and value to perform your mapping op, so, keySet() is also not appropriate. You're looking for entrySet(), which gives you a set of all k/v pairs, juts what we need.
That gets us to:
map.entrySet().stream()
.map(e -> String.format("%s-%s", e.getKey(), e.getValue()))
.collect(Collectors.toList());
Your original code makes no effort to treat a single value from a map (which is a List<String>) as separate values; you just call .toString() on the entire ordeal, and be done with it. This means the produced string looks like, say, [Hello, World] given a map value of List.of("Hello", "World"). If you don't want this, you still don't want flatmap, because streams are also homogenous - the values in a stream are all of the same kind, and thus a stream of 'key1 value1 value2 key2 valueA valueB' is not what you'd want:
map.entrySet().stream()
.map(e -> String.format("%s-%s", e.getKey(), myPrint(e.getValue())))
.collect(Collectors.toList());
public static String myPrint(List<String> in) {
// write your own algorithm here
}
Stream API just isn't the right tool to replace that myPrint method.
A third alternative is that you want to smear out the map; you want each string in a mapvalue's List<String> to first be matched with the key (so that's re-stating that key rather a lot), and then do something to that. NOW flatMap IS appropriate - you want a stream of k/v pairs first, and then do something to that, and each element is now of the same kind. You want to turn the map:
key1 = [value1, value2]
key2 = [value3, value4]
first into a stream:
key1:value1
key1:value2
key2:value3
key2:value4
and take it from there. This explodes a single k/v entry in your map into more than one, thus, flatmapping needed:
return map.entrySet().stream()
.flatMap(e -> e.getValue().stream()
.map(v -> String.format("%s-%s", e.getKey(), v))
.collect(Collectors.toList());
Going inside-out, it maps a single entry within a list that belongs to a single k/v pair into the string Key-SingleItemFromItsList.
Adding my two cents to excellent answer by #rzwitserloot. Already flatmap and map is explained in his answer.
List<String> resultLists = myMap.entrySet().stream()
.flatMap(mapEntry -> printEntries(mapEntry.getKey(),mapEntry.getValue())).collect(Collectors.toList());
System.out.println(resultLists);
Splitting this to a separate method gives good readability IMO,
private static Stream<String> printEntries(String key, List<String> values) {
return values.stream().map(val -> String.format("%s-%s",key,val));
}

Partially Flatten Nested Collections in Java using Lambdas

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

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)

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.

Categories

Resources