How to apply IF logic during stream pipeline calculation? - java

I have a code like this. But it looks a bit ugly
Set<String> strings= new HashSet(Arrays.asList("str1", "str2", "str3" ));
Optional.of(strings.stream()
.filter(myMap::containsKey)
.collect(Collectors.toMap(Function.identity(), myMap::get)))
.map(stringListMap -> stringListMap.isEmpty() ? null : stringListMap)
.orElse(myMap)
.entrySet()
.stream()
.flatMap(...)
...
Is there way to avoid collection in the middle of pipeline ?

I think the more imperative solution is actually more readable here.
Map<String, Object> actualMap = myMap; // equiv. to your .orElse(myMap)
if (myMap.keySet().stream().anyMatch(strings::contains)) {
actualMap = strings.stream().collect(Collectors.toMap(Function.identity(), myMap::get));
}
actualMap.entrySet().stream()
.flatMap(...)
Java's a nice language because it allows you to be pragmatic about when you apply functional ideas. Don't feel like you have to use them all the time.

I'm not adding much here - very similar to Michael's answer. But this avoids creating streams out of both myMap and strings.
Map<String, Object> mappedValues = strings.stream()
.filter(myMap::containsKey)
.collect(Collectors.toMap(Function.identity(), myMap::get)));
(mappedValues.isEmpty()
? myMap
: mappedValues).entrySet()
.stream()
.flatMap(...)

It's a sort of a late answer, but the following solution does not use Optional and does not create a mapping if no key from strings exists in myMap at the cost of duplicate running the stream on the smaller strings set:
(strings.stream().anyMatch(myMap::containsKey)
? strings.stream()
.filter(myMap::containsKey)
.collect(Collectors.toMap(k -> k, myMap::get))
: myMap
)
.entrySet().stream()
.flatMap()

Related

Grouping of inner Maps with Java Streams

i have following structure:
Map<String,Map<String,Map<String,Integer>>>
Now i want to disregard the First-level-Maps and group (and sum up) the 3rd-Level-Maps according to the key of the 2nd-Level-Maps.
To Clarify some example-Entries:
Entry 1: ["1"["A"[[a,1];[b,2]];"B"[[a,3];[c,1]]]]
Entry 2: ["2"["A"[[b,2];[c,1]];"B"[[a,5];[b,0]]]]
Desired output:
Entry 1: ["A"[[a,1];[b,4];[c,1]]]
Entry 4: ["B"[[a,8];[b,0];[c,1]]]
So to do this I first group my Entry-stream according to my 2nd-Level-Keys ("A","B") and, if nothing else done, end up with a structure like the following:
Map<String,List<Entry<String,Map<String,Integer>>>>
And here is where I am stuck. How do i go about getting my Map<String,Integer>from my List of Entries (for each outer Map, secifically)?
The simple code which I assume is guaranteed to be needed:
initialMap.values().stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey));
Summary:
How do I transform a Map<String,Map<String,Map<String,Integer>>> to a Map<String<Map<String,Integer>>, disregarding the outermost Map, grouping my innermost Maps according to my 2nd-Layer-Keys and summing my Integer-values by key-values of the Innermost Map.
Additionally the outermost Maps each have a Key-Value-Pair for each 2nd-Level-Map, so each will have the same 2nd-Level-Keys. In the 3rd-Level-Keysets can be Keys not found in other 3rd-Level-Maps
Map<String, Map<String, Integer>> result =
initialMap
.values()
.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.groupingBy(e -> mapToFirstEntry(e.getValue()).getKey(),
Collectors.summingInt(e -> mapToFirstEntry(e.getValue()).getValue()))));
but it assumes that a third-level Map<String, Integer> contains one entry and there is a method to get that entry:
public static Map.Entry<String, Integer> mapToFirstEntry(Map<String, Integer> map) {
return map.entrySet().iterator().next();
}
If you have the liberty of using Java9, I would suggest you to use the flatMapping collector to solve this problem. This approach is much more readable and generates less visual clutter to me. Here's how it looks.
Map<String, Map<String, Integer>> summaryMap = map.values().stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.flatMapping(e -> e.getValue().entrySet().stream(),
Collectors.groupingBy(Map.Entry::getKey,
Collectors.summingInt(Map.Entry::getValue)))));
This program produces the following output:
{A={a=1, b=4, c=1}, B={a=8, b=0, c=1}}
A thing to keep in mind here: streams conceptually represent a single element coming down through a "pipe" of sorts. It's always single element when the stream runs, no matter if source has one, multiple or infinite number of elements backed up in total.
What you're trying to do here is represent several nested loops, along the lines of:
Map<String, Map<String, Integer>> result = new HashMap<>();
for (Map<String, Map<String, Integer>> firstMap : inputMap.values()) {
for (Entry<String, Map<String, Integer>> firstEntry : firstMap.entrySet()) {
String upperCaseKey = firstEntry.getKey();
Map<String, Ingeter> resultEntry = result.computeIfAbsent(
upperCaseKey,
_k -> new HashMap<>());
for (Entry<String, Integer> secondEntry : firstEntry.getValue().entrySet()) {
resultEntry.merge(secondEntry.getKey(), secondEntry.getValue(), Integer::sum);
}
}
}
Among the better ways to do it with streams would be via Collector composition:
inputMap.values().stream()
.flatMap(map -> map.entrySet().stream())
.flatMap(firstEntry -> firstEntry.getValue()
.entrySet().stream()
.map(secondEntry -> new SimpleImmutableEntry(
firstEntry.getKey(),
secondEntry)
)
)
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.groupingBy(
compositeEntry -> compositeEntry.getValue().getKey(),
Collectors.summingInt(compositeEntry -> compositeEntry.getValue().getValue())
)
));
That should do the trick in general, but note how I had to first build up a composite entry, to keep element count as 1, and then nested two grouping collectors. This is why I'm of the opinion that tasks like yours aren't good fit for the API. It's also very likely to require a little help from you to compiler, as it may struggle to infer all the types correctly.
Also note, that this is not the only way to do it: the Stream API is very flexible, and you're likely to see many more other ways to do the same.

Java 8 streams, convert List of object to Map<String, Set<String>>

I have already gone through few examples and those did not work for me.
Here is what I am trying to do:
I have a List<SomeClass> of the following class:
class SomeClass {
String rid;
String name;
...
}
The values in my List look like this:
SomeClass(1,"apple")
SomeClass(1,"banana")
SomeClass(1,"orange")
SomeClass(2,"papaya")
SomeClass(2,"peaches")
SomeClass(3,"melons")
I want to convert the above List into a Map<String, Set<String>>, where key is rid and value is Set of name field.
To solve this using Java Streams I am using groupingBy and I could come to below solution:
someClassList
.stream()
.map(SomeClass::getName)
.collect(
Collectors.groupingBy(
SomeClass::getRid, Collectors.toSet()));
But this gives me compilation error.
How do I solve this and what is the problem with my approach?
When you call .map(SomeClass::getName) on your Stream<SomeClass>, you get a Stream<String>. You can't execute collect(Collectors.groupingBy(SomeClass::getRid,...)) on a Stream<String> (you can only execute it on a Stream<SomeClass>).
Therefore your map step is wrong.
You need to pass the Collector returned by Collectors.mapping() to Collectors.groupingBy() in order to map the SomeClass instances to Strings after they are grouped by getRid.
Map<String, Set<String>> map =
someClassList.stream()
.collect(Collectors.groupingBy(SomeClass::getRid,
Collectors.mapping(SomeClass::getName,
Collectors.toSet())));
Although not as readable as the groupingBy collector; you can use the toMap collector just as well:
myList.stream()
.collect(toMap(SomeClass::getRid, e -> new HashSet<>(singleton(e.getName())),
(l, r) -> {l.addAll(r); return l;}));
Ensure that you have the necessary imports for singleton and toMap or you can just use Collectors.toMap(...) and Collections.singleton(...) respectively.

java8 reduce with useless combiner

I need to apply a list of regex to a string, so I thought to use java8 map reduce:
List<SimpleEntry<String, String>> list = new ArrayList<>();
list.add(new SimpleEntry<>("\\s*\\bper\\s+.*$", ""));
list.add(new SimpleEntry<>("\\s*\\bda\\s+.*$", ""));
list.add(new SimpleEntry<>("\\s*\\bcon\\s+.*$", ""));
String s = "Tavolo da cucina";
String reduced = list.stream()
.reduce(s, (v, entry) -> v.replaceAll(entry.getKey(), entry.getValue()) , (c, d) -> c);
Actually this code may be is not very beautiful, but it works. I know this cannot be parallelised and for me is ok.
Now my question is: is there any chance with Java8 (or higher version) to write something more elegant? I mean also avoiding to add the useless combiner function.
Inspired by Oleksandr's comment and Holger I wrote this
String reduced = list.stream()
.map(entry->
(Function<String, String>) v -> v.replaceAll(entry.getKey(), entry.getValue()))
.reduce(Function.identity(), Function::andThen)
.apply(s);
This also reduce all entries to a function composition.
Here's another, interesting approach: reduce all entries to a function composition, then apply that composed function on the original input:
String result = list.stream()
.map(entry ->
(Function<String, String>) text ->
text.replaceAll(entry.getKey(), entry.getValue()))
//following op also be written as .reduce(Function::compose) (see comment by Eugene)
.reduce((f1, f2) -> f1.andThen(f2)) //compose functions
.map(func -> func.apply(s)) //this basically runs all `replaceAll`
.get();
The result of this is your expected string. While this function composition is not intuitive, it nonetheless seems to fit the idea that your original list is in fact a sort of "transformation logic" chain.

Java-Stream, toMap with duplicate keys

So there might be one abc for several payments, now I have:
//find abc id for each payment id
Map<Long, Integer> abcIdToPmtId = paymentController.findPaymentsByIds(pmtIds)
.stream()
.collect(Collectors.toMap(Payment::getAbcId, Payment::getPaymentId));
But then I reallize this could have duplicate keys, so I want it to return a
Map<Long, List<Integer>> abcIdToPmtIds
which an entry will contain one abc and his several payments.
I know I might can use groupingBy but then I think I can only get Map<Long, List<Payments>> .
Use the other groupingBy overload.
paymentController.findPaymentsByIds(pmtIds)
.stream()
.collect(
groupingBy(Payment::getAbcId, mapping(Payment::getPaymentId, toList());
Problem statement: Converting SimpleImmutableEntry<String, List<String>> -> Map<String, List<String>>.
For Instance you have a SimpleImmutableEntry of this form [A,[1]], [B,[2]], [A, [3]] and you want your map to looks like this: A -> [1,3] , B -> [2].
This can be done with Collectors.toMap but Collectors.toMap works only with unique keys unless you provide a merge function to resolve the collision as said in java docs.
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator-
So the example code looks like this:
.map(returnSimpleImmutableEntries)
.collect(Collectors.toMap(SimpleImmutableEntry::getKey,
SimpleImmutableEntry::getValue,
(oldList, newList) -> { oldList.addAll(newList); return oldList; } ));
returnSimpleImmutableEntries method returns you entries of the form [A,[1]], [B,[2]], [A, [3]] on which you can use your collectors.
With Collectors.toMap:
Map<Long, Integer> abcIdToPmtId = paymentController.findPaymentsByIds(pmtIds)
.stream()
.collect(Collectors.toMap(
Payment::getAbcId,
p -> new ArrayList<>(Arrays.asList(p.getPaymentId())),
(o, n) -> { o.addAll(n); return o; }));
Though it's more clear and readable to use Collectors.groupingBy along with Collectors.mapping.
You don't need streams to do it though:
Map<Long, Integer> abcIdToPmtId = new HashMap<>();
paymentController.findPaymentsByIds(pmtIds).forEach(p ->
abcIdToPmtId.computeIfAbsent(
p.getAbcId(),
k -> new ArrayList<>())
.add(p.getPaymentId()));

Flatten a Map<Integer, List<String>> to Map<String, Integer> with stream and lambda

I would like to flatten a Map which associates an Integer key to a list of String, without losing the key mapping.
I am curious as though it is possible and useful to do so with stream and lambda.
We start with something like this:
Map<Integer, List<String>> mapFrom = new HashMap<>();
Let's assume that mapFrom is populated somewhere, and looks like:
1: a,b,c
2: d,e,f
etc.
Let's also assume that the values in the lists are unique.
Now, I want to "unfold" it to get a second map like:
a: 1
b: 1
c: 1
d: 2
e: 2
f: 2
etc.
I could do it like this (or very similarly, using foreach):
Map<String, Integer> mapTo = new HashMap<>();
for (Map.Entry<Integer, List<String>> entry: mapFrom.entrySet()) {
for (String s: entry.getValue()) {
mapTo.put(s, entry.getKey());
}
}
Now let's assume that I want to use lambda instead of nested for loops. I would probably do something like this:
Map<String, Integer> mapTo = mapFrom.entrySet().stream().map(e -> {
e.getValue().stream().?
// Here I can iterate on each List,
// but my best try would only give me a flat map for each key,
// that I wouldn't know how to flatten.
}).collect(Collectors.toMap(/*A String value*/,/*An Integer key*/))
I also gave a try to flatMap, but I don't think that it is the right way to go, because although it helps me get rid of the dimensionality issue, I lose the key in the process.
In a nutshell, my two questions are :
Is it possible to use streams and lambda to achieve this?
Is is useful (performance, readability) to do so?
You need to use flatMap to flatten the values into a new stream, but since you still need the original keys for collecting into a Map, you have to map to a temporary object holding key and value, e.g.
Map<String, Integer> mapTo = mapFrom.entrySet().stream()
.flatMap(e->e.getValue().stream()
.map(v->new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v)))
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
The Map.Entry is a stand-in for the nonexistent tuple type, any other type capable of holding two objects of different type is sufficient.
An alternative not requiring these temporary objects, is a custom collector:
Map<String, Integer> mapTo = mapFrom.entrySet().stream().collect(
HashMap::new, (m,e)->e.getValue().forEach(v->m.put(v, e.getKey())), Map::putAll);
This differs from toMap in overwriting duplicate keys silently, whereas toMap without a merger function will throw an exception, if there is a duplicate key. Basically, this custom collector is a parallel capable variant of
Map<String, Integer> mapTo = new HashMap<>();
mapFrom.forEach((k, l) -> l.forEach(v -> mapTo.put(v, k)));
But note that this task wouldn’t benefit from parallel processing, even with a very large input map. Only if there were additional computational intense task within the stream pipeline that could benefit from SMP, there was a chance of getting a benefit from parallel streams. So perhaps, the concise, sequential Collection API solution is preferable.
You should use flatMap as follows:
entrySet.stream()
.flatMap(e -> e.getValue().stream()
.map(s -> new SimpleImmutableEntry(e.getKey(), s)));
SimpleImmutableEntry is a nested class in AbstractMap.
Hope this would do it in simplest way. :))
mapFrom.forEach((key, values) -> values.forEach(value -> mapTo.put(value, key)));
This should work. Please notice that you lost some keys from List.
Map<Integer, List<String>> mapFrom = new HashMap<>();
Map<String, Integer> mapTo = mapFrom.entrySet().stream()
.flatMap(integerListEntry -> integerListEntry.getValue()
.stream()
.map(listItem -> new AbstractMap.SimpleEntry<>(listItem, integerListEntry.getKey())))
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
Same as the previous answers with Java 9:
Map<String, Integer> mapTo = mapFrom.entrySet()
.stream()
.flatMap(entry -> entry.getValue()
.stream()
.map(s -> Map.entry(s, entry.getKey())))
.collect(toMap(Entry::getKey, Entry::getValue));

Categories