Java8 Stream: groupingBy and create Map - java

I want to get the following Data structure: Map<String, Map<String, Integer>>
Given is a class either containing the fields als primitives (position, destination, distance) or as a key (position) plus map (target). From each unique position one can target to many destinations (by distance).
private static class LocationPair {
String position, destination;
int distance;
}
Map<String, Map<String, Integer>> locations = locationPairs.stream()
.collect(Collectors.groupingBy(pair -> pair.position, Collectors.toMap(pair.destination, pair.distance)));
private static class LocationPair {
String position;
Map<String, Integer> target = Collections.singletonMap(destination, distance);
}
Map<String, Map<String, Integer>> locations = locationPairs.stream()
.collect(Collectors.groupingBy(pair -> pair.position, Collectors.mapping(pair -> pair.target)));
Regarding the second code-snippet: The result should be the same as in the first code.The only difference is, that the provided data in LocationPair have been further processed so that destination and distance have been put already into their target-Map.
I know this must be possible, but somehow I can't figure it out how to get it done. The stream-code snippets above shall show what I mean although I know that they aren't working.
Many thanks for any help

The code mostly looked correct. Minor tweaks got it working.
public static Map<String, Map<String, Integer>> locations(List<LocationPair> locationPairs) {
return locationPairs.stream()
.collect(
Collectors.groupingBy(LocationPair::getPosition, Collectors.toMap(LocationPair::getDestination, LocationPair::getDistance)));
}
Using variables instead of method references, this becomes -
locationPairs.stream()
.collect(
Collectors.groupingBy(tmp -> tmp.position, Collectors.toMap(tmp2 -> tmp2.destination, tmp2 -> tmp2.distance)));
Hope this helps. Let me know in-case I missed something

Related

How can I properly make a multilevel map using lambdas?

I'm comparing files in folders (acceptor & sender) using JCIFS. During comparation two situations may occur:
- file not exists at acceptor
- file exists at acceptor
I need to get a map, where compared files are groupped by mentioned two types, so i could copy non-existing files or chech size and modification date of existing...
I want to make it using lambdas and streams, because i woult use parallel streams in near future, and it's also convinient...\
I've managed to make a working prototype method that checks whether file exists and creates a map:
private Map<String, Boolean> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
Map.Entry::getValue)));
.collect(collectingAndThen(
toMap(Map.Entry::getKey, Map.Entry::getValue),
Collections::<String,Boolean> unmodifiableMap));
}
but i cant add higher level grouping by map value...
I have such a non-working piece of code:
private Map<String, Boolean> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
.collect(groupingBy(
Map.Entry::getValue,
groupingBy(Map.Entry::getKey, Map.Entry::getValue)));
}
}
My code can't compile, because i missed something very important.. Could anyone help me please and exlain how to make this lambda correct?
P.S. arrays from method parameters are SmbFiles samba directories:
private final String master = "smb://192.168.1.118/mastershare/";
private final String node = "smb://192.168.1.118/nodeshare/";
SmbFile masterDir = new SmbFile(master);
SmbFile nodeDir = new SmbFile(node);
Map<Boolean, <Map<String, Boolean>>> resultingMap = compareFiles(masterDir, nodeDir);
Collecting into nested maps with the same values, is not very useful. The resulting Map<Boolean, Map<String, Boolean>> can only have two keys, true and false. When you call get(true) on it, you’ll get a Map<String, Boolean> where all string keys redundantly map to true. Likewise, get(false) will give a you map where all values are false.
To me, it looks like you actually want
private Map<Boolean, Set<String>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.collect(partitioningBy(Arrays.asList(acceptor)::contains, toSet()));
}
where get(true) gives you a set of all strings where the predicate evaluated to true and vice versa.
partitioningBy is an optimized version of groupingBy for boolean keys.
Note that Stream.of(acceptor).anyMatch(s::equals) is an overuse of Stream features. Arrays(acceptor).contains(s) is simpler and when being used as a predicate like Arrays.asList(acceptor)::contains, the expression Arrays.asList(acceptor) will get evaluated only once and a function calling contains on each evaluation is passed to the collector.
When acceptor gets large, you should not consider parallel processing, but replacing the linear search with a hash lookup
private Map<Boolean, Set<String>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.collect(partitioningBy(new HashSet<>(Arrays.asList(acceptor))::contains, toSet()));
}
Again, the preparation work of new HashSet<>(Arrays.asList(acceptor)) is only done once, whereas the contains invocation, done for every element of sender, will not depend on the size of acceptor anymore.
I've managed to solve my problem. I had a type mismatch, so the working code is:
private Map<Boolean, Map<String, Boolean>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
.collect(collectingAndThen(
groupingBy(Map.Entry::getValue, toMap(Map.Entry::getKey, Map.Entry::getValue)),
Collections::<Boolean, Map<String, Boolean>> unmodifiableMap));
}

Java 8 Stream Create an Object from Map of Objects

I just started with learning and implementing collections via the Java 8 stream API. I have one class:
public class Discount {
int amount;
String lastMarketingRegion;
Discount (int amount, String lastMarketingRegion) {
this.amount = amount;
this.lastMarketingRegion= lastMarketingRegion;
}
public int getAmount() { return amount; }
public String getLastMarketingRegion() { return lastMarketingRegion; }
public String toString() {
return String.format("{%s,\"%s\"}", amount, lastMarketingRegion);
}
}
And I am given with the following:
Map<String, Discount> prepaid = new HashMap<String, Discount>();
prepaid.put("HAPPY50", new Discount(100, "M1"));
prepaid.put("LUCKY10", new Discount(10, "M2"));
prepaid.put("FIRSTPAY", new Discount(20, "M3"));
Map<String, Discount> otherBills = new HashMap<String, Discount>();
otherBills.put("HAPPY50", new Discount(60, "M4"));
otherBills.put("LUCKY10", new Discount(7, "M5"));
otherBills.put("GOOD", new Discount(20, "M6"));
List<Map<String, Discount>> discList = new ArrayList<Map<String, Discount>>();
discList.add(prepaid);
discList.add(otherBills);
So, basically I have a list of Discount maps of all discount codes for different payment types.
Requirement is to create a single map with all the discount codes across all payment types with sum_of_amount and the last_region:
Map<String, Discount> totalDiscounts =
{LUCKY10={17, "M5"}, FIRSTPAY={20, "M3"}, HAPPY50={160, "M4"}, GOOD={20, "M6"}}
I am able to get:
Map<String, Integer> totalDiscounts =
{LUCKY10=17, FIRSTPAY=20, HAPPY50=160, GOOD=20}
by using the following code:
Map<String, Integer> afterFormatting = discList.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.summingInt(map -> map.getValue().amount)));
but I need a Discount object also with the region.
I need a collection of Discount objects where the amount is the total of the amounts of same key and region is from otherBills.
Any help would be much appreciated. Thank You.
Edit 1 -
For the sake of simplicity, please consider lastMarketingRegion to have same value for a discount code.
I also tried to explain it via diagram -
From comments
Why do you expect "LUCKY10" - "M5" when you have "M2" and "M5" entries for LUCKY10?
because otherBills has more priority than prepaid
You can use Collectors.toMap for this. The last argument to it is the mergeFunction that merges two Discounts that had same String key in the map.
Map<String, Discount> totalDiscounts = discList.stream()
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(discount1, discount2) -> new Discount(discount1.getAmount() + discount2.getAmount(),
discount2.getLastMarketingRegion())));
Since the stream generated out of a list is ordered, the discount2 Discount will be the one from the otherBills map and hence I'm picking the region of it.
If you have constructed the list by adding otherBills followed by prepaid, then this will have a different output.
Relying on the encounter order makes this a not-a-great-solution.
(If you are going to assume we process entries from the second map after processing the first, why merge them in the first place?)
See my other answer that uses Map.merge
If you have just two maps, then rather than going for a stream-based solution (my other answer), you can use Map.merge for this.
Here, we make a copy of the prepaid map. Then we iterate through the otherBills map. For each key
If the mapping does not exist, it adds it to the map (result map)
If the mapping already exists, we construct a new Discount object whose amount is the sum of amounts of the Discount object already present in the map (the one from prepaid) and the current Discount object (the one from otherBill). It takes the region of the Discount object from the otherBill map.
Map<String, Discount> result = new HashMap<>(prepaid);
otherBills.forEach((k, v) -> result.merge(k, v, (discountFromPrepaid, discountFromOtherBill) ->
new Discount(discountFromPrepaid.getAmount() + discountFromOtherBill.getAmount(),
discountFromOtherBill.getLastMarketingRegion())));

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

How to make these simple and beautiful?

When I put a (KEY, VALUE) into a map such as Map<String, List<String>>, and I want to check if the KEY is existed first to decide if I have to make a new List, usually My Java Code looks like this:
Map<String, List<String>> example = new HashMap<>();
public void put(String k, String v){
if(example.containsKey(k)){
example.get(k).add(v);
return;
}
List<String> vs = new ArrayList<>();
vs.add(v);
example.put(k,vs);
}
It doesn't looks very nice. Is there any way to make it more simple and more beautiful?
If you have Java 8 you can write this as one line:
example.computeIfAbsent(k, key -> new ArrayList<>()).add(v);
This uses a lambda, so the new ArrayList is only created if required.
(k and key need to have different names, as they are different variables)
Map<String, List<String>> example = new HashMap<>();
public void put(String k, String v){
if (!example.containsKey(k)){
example.put(k, new ArrayList<>();
}
example.get(k).add(v);
}
Arguably, this is slightly wasteful - requiring you to get the list you just put - but to my eye it is much cleaner and more expressive.
If you can't use other libraries, or java 8, you could wrap the whole map and construct in a class of you own.
With your own class you:
Confine the messiness to one place.
Hide the face you are using a Map behind the scenes.
Have a place to move any additional logic to.

converting static 2D String array to HashMap

What is the easiest way to convert a 2D array of Strings into a HashMap?
For example, take this:
final String[][] sheetMap = { /* XSD Name, XSL Sheet Name */
{"FileHeader", "FileHeader"},
{"AccountRecord", "AccountRecord"},
{"DriverCardRecord", "DriverCardRecord"},
{"AssetCardRecord", "AssetCardRecord"},
{"SiteCardRecord", "SiteCardRecord"}
};
This is most likely going to be loaded from a file and will be much bigger.
final Map<String, String> map = new HashMap<String, String>(sheetMap.length);
for (String[] mapping : sheetMap)
{
map.put(mapping[0], mapping[1]);
}
If you just want to initialize your map in a convenient way, you can use double brace initialization:
Map<String, String > sheetMap = new HashMap<String, String >() {{
put( "FileHeader", "FileHeader" );
put( "AccountRecord", "AccountRecord" );
put( "DriverCardRecord", "DriverCardRecord" );
put( "AssetCardRecord", "AssetCardRecord" );
put( "SiteCardRecord", "SiteCardRecord" );
}};
As a slightly cleaner alternative to tradeJmark answer:
String[][] arr = // your two dimensional array
Map<String, String> arrMap = Arrays.stream(arr).collect(Collectors.toMap(e -> e[0], e -> e[1]));
// Sanity check!
for (Entry<String, String> e : arrMap.entrySet()) {
System.out.println(e.getKey() + " : " + e.getValue());
}
Wait; if this is going to be loaded from a file, don't go through the intermediate array step! You would have had to load it all first before creating the array or you wouldn't know the size for the array. Just create a HashMap and add each entry as you read it.
The existing answers work well, of course, but in the interest of continually updating this site with new info, here's a way to do it in Java 8:
String[][] arr = {{"key", "val"}, {"key2", "val2"}};
HashMap<String, String> map = Arrays.stream(arr)
.collect(HashMap<String, String>::new,
(mp, ar) -> mp.put(ar[0], ar[1]),
HashMap<String, String>::putAll);
Java 8 Streams are awesome, and I encourage you to look them up for more detailed info, but here are the basics for this particular operation:
Arrays.stream will get a Stream<String[]> to work with.
collect takes your Stream and reduces it down to a single object that collects all of the members. It takes three functions. The first function, the supplier, generates a new instance of an object that collects the members, so in our case, just the standard method to create a HashMap. The second function, the accumulator, defines how to include a member of the Stream into the target object, in your case we simply want to put the key and value, defined as the first and second value from each array, into the map. The third function, the combiner, is one that can combine two of the target objects, in case, for whatever reason, the JVM decided to perform the accumulation step with multiple HashMaps (in this case, or whatever other target object in another case) and then needs to combine them into one, which is primarily for asynchronous execution, although that will not typically happen.
More concise with streams would be:
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.util.Map;
...
public static Map<String, String> asMap(String[][] data) {
return stream(data).collect(toMap( m->m[0], m->m[1] ));
}
...
Java 8 way
public static Map<String, String> convert2DArrayToMap(String[][] data){
return Arrays.stream(data).collect(Collectors.toMap(m -> m[0], m -> m[1]));
}
with loop
public static Map<String, String> convert2DArrayToMap(String[][] data)
{
Map<String, String> map = new HashMap<>();
for (String[] m : data)
{
if (map.put(m[0], m[1]) != null)
{
throw new IllegalStateException("Duplicate key");
}
}
return map;
}

Categories

Resources