Streams on nested map - java

I have the following usecase. I have a nested map with following structure:
Map<String, Map<WorkType, List<CostLineItem>>>
I have to iterate over the map and get the list of CLObject. If the single entry in the list has identifier as null. I have to generate the unique identifier per EnumType. I am not sure how to do it with streams? Following iteration logic will make clear what i want to accomplish
for(Map.Entry<String, Map<WorkType, List<CostLineItem>>> cliByWorkTypeIterator: clisByWorkType.entrySet()) {
Map<WorkType, List<CostLineItem>> entryValue = cliByWorkTypeIterator.getValue();
for(Map.Entry<WorkType, List<CostLineItem>>cliListIterator : entryValue.entrySet()) {
List<CostLineItem> clis = cliListIterator.getValue();
//if any CLI settlementNumber is zero this means we are in standard upload
//TODO: Should we use documentType here? Revisit this check while doing dispute file upload
if(clis.get(0).getSettlementNumber() == null) {
clis.forEach(f -> f.toBuilder().settlementNumber(UUID.randomUUID().toString()).build());
}
}
}
Nested loop makes the code bit boiler plate and dirty. Can someone help me with streams here?

You can use flatMap to iterate over all the List<CostLineItem> values of all the inner Maps.
clisByWorkType.values() // returns Collection<Map<WorkType, List<CostLineItem>>>
.stream() // returns Stream<Map<WorkType, List<CostLineItem>>>
.flatMap(v->v.values().stream()) // returns Stream<List<CostLineItem>>
.filter(clis -> clis.get(0).getSettlementNumber() == null) // filters that Stream
.forEach(clis -> {do whatever logic you need to perform on the List<CostLineItem>});

The following is equivalent to your for-loop:
clisByWorkType.entrySet()
.map(Map.Entry::getValue) // cliByWorkTypeIterator.getValue();
.flatMap(m -> m.entrySet().stream())
.map(Map.Entry::getValue)
.map(CostLineItem::getValue)
.filter(clis.get(0).getSettlementNumber() == null) //filter before flattening
.flatMap(List::stream)
.forEach(f -> f.toBuilder().settlementNumber(UUID.randomUUID().toString()).build());

clisByWorkType.values()
.stream()
.flatMap(e -> e.values().stream())
.filter(clis -> clis.get(0).getSettlementNumber() == null)
.flatMap(Collection::stream)
.forEach(f -> f.toBuilder().settlementNumber(UUID.randomUUID().toString()).build());

Related

Java8 Streams: How to preserve the value before map to be accessed by collect/groupingBy functions

I'm using Java8 Streams to iterate through a list and for each of the element I invoke map and then I need to aggregate the results. My problem is that when I call groupingBy I also need to access the original object before calling map. Here is a snippet:
list.stream() //
.filter(item -> item.getType() == HUMAN) //
.map(item -> manager.itemToHuman(item.getId())) //
.filter(Objects::nonNull) //
.collect(Collectors.groupingBy(Human::getAge, Collectors.summarizingLong(item.getCount())));
The problem is with the call to Collectors.summarizingLong(item.getCount()) since item at this point is NOT accessible. Is there an elegant way to overcome this?
After doing map() stream transformed into Stream<Human> so you can't use item object in the collector.
You can transform item into a pair of Human object and count using SimpleEntry then use it on the collector.
list.stream()
.filter(item -> item.getType() == HUMAN)
.map(item ->
new AbstractMap.SimpleEntry<>(manager.itemToHuman(item.getId()), item.getCount()))
.filter(entry -> Objects.nonNull(entry.getKey()))
.collect(Collectors.groupingBy(entry -> entry.getKey().getAge(),
Collectors.summarizingLong(Map.Entry::getValue)));

Java8 streams with Map?

I have the following Map (each key is a String and each value is a List<Message>)
My map is like this :
1st entry :"MONDAY" -> [message1, message2, message3]
2nd entry : "TUESDAY" -> [message4, message5]
...
My goal is to change each message content :
I was thinking about this :
map.entrySet().stream().peek(entry -> {
entry.getValue().stream().peek(m -> m.setMessage(changeMessage()))
})
But don't know how to finish and do it properly.
Unfortunately, java-stream doesn't provide a straightforward way to change the Map values without violating the Side-effects principle:
Side-effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards.
Here is a possible solution:
Map<String, List<Message>> = map.entrySet().stream()
.map(e -> { // iterate entries
e.setValue(e.getValue().stream() // set a new value
.map(message -> {
message -> message.setMessage(changeMessage()); // .. update message
return message;}) // .. use it
.collect(Collectors.toList())); // return as a List
return e;}) // return an updated value
.collect(Collectors.toMap(Entry::getKey, Entry::getValue)); // collec to a Map
However, Java provides a well-known for-each feature to achieve your goal in a more direct way which is more readable:
for (List<Message> list: map.values()) {
for (Message message: list) {
message.setMessage(changeMessage());
}
}
Iterate map, change each element of list again put the collected list on the same key of the map.
map.forEach((k,v)->{
map.put(k, v.stream().map(i->i+"-changed").collect(Collectors.toList()));
});
If you just want to update the message of all messages there is no need to use the whole entry set. You can just stream the values of your map and map the items. The use forEach() to update them:
map.values().stream().flatMap(List::stream)
.forEach(m -> m.setMessage(changeMessage(m.getMessage())));
If you need the key to change the message you can use this:
map.forEach((key, messages) -> messages.forEach(m ->
m.setMessage(changeMessage(key, m.getMessage()))));

Java 8 stream with two Lists

I have a method takes 2 lists as parameters and as you can see in the method body I want to do some filtering and returning the result to the caller. I wanted to convert this code to the Java 8 stream with lambda expressions but I couldn't figure that out. I ended up creating more than one stream for this and it beats the purpose of this refactoring (IMHO). What I wanted to know is that how I do, in a simple way, refactor this into just one stream?
public Set<CustomerTrack> getCustomerTracks(List<CusomerTrack> tracks, List<Customer> customers) {
Set<CustomerTrack> tracksToSave = new HashSet<>();
for (Customer customer : customers) {
if (customer.getTrack() == null) {
continue;
}
Long allowedTrackId = customer.getTrack().getId();
for (CustomerTrack track : tracks) {
if (Long.valueOf(track.getId()).equals(allowedTrackId)) {
tracksToSave.add(track);
}
}
}
return tracksToSave;
}
Seems that this is what you are after:
customers.stream()
.filter(c -> c.getTrack() != null)
.map(c -> c.getTrack().getId())
.flatMap(id -> tracks.stream().filter(track -> Long.valueOf(track.getId()).equals(id)))
.collect(Collectors.toSet());
Just note that for each id you are iterating the entire list of tracks; this has O(n*m) complexity. This is generally see as bad and you can improve it.
To make it better you would first create a HashSet of ids from Customer; having that HashSet you can now call contains on it with the ids you are interested in, since contains has a time complexity of O(1) (it's really called amortized complexity of O(1)). So now your complexity becomes O(n) + O(1), but since O(1) is a constant, it's really O(n) - much better that what you had before. In code:
Set<Long> set = customers.stream()
.filter(c -> c.getTrack() != null)
.map(c -> c.getTrack().getId())
.collect(Collectors.toSet());
Set<CusomerTrack> tracksToSave = tracks.stream()
.filter(track -> set.contains(track.getId())
.collect(Collectors.toSet()));
An additional way favoring method reference usage :
Set<Track> tracks =
customers.stream()
.map(Customer::getTrack) // customer to track
.filter(Objects::nonNull) // keep non null track
.map(Track::getId) // track to trackId
.flatMap(trackId -> tracks.stream() // collect tracks matching with trackId
.filter(t-> Long.valueOf(t.getId()).equals(trackId))
)
.collect(toSet());
Firstly you can create a Set of allowed Ids:
Set<Long> collect = customers.stream()
.filter(customer -> customer.getTrack() != null)
.map(customer -> customer.getTrack().getId())
.collect(Collectors.toSet());
Then you can filler your track collection
Set<CusomerTrack> tracksToSave = tracks.stream()
.filter(track -> collect.contains(Long.valueOf(track.getId())))
.collect(Collectors.toSet());
Try this one
customers.stream()
.filter(customer -> customer.getTrack() != null)
.map(c -> c.getTrack().getId())
.forEach(allowedTrackId -> {
tracks.stream()
.filter(track -> Long.valueOf(track.getId()).equals(allowedTrackId))
.forEach(tracksToSave::add);
});
The important Operator here is flatMap
Set<CustomerTrack> tracksToSave = customers.stream()
.map(Customer::getTrack)
.filter(track -> track != null)
.flatMap(track -> {
tracks.stream.filter(it -> Long.valueOf(it.getId()).equals(track.getId())))
.collect(Collectors.toSet());
You need to filter the null values first and then filter it with the list of customerTrack.
Hope this answer helps you.
return customers.stream().map(cust-> cust.track).filter(track -> track != null).
collect(Collectors.toList())
.stream().filter(track-> customerTracks.stream()
.anyMatch(ele -> ele.getId() ==
track.getId())).collect(Collectors.toSet());
You could try something like this
customers
.stream()
.map(Customer::getTrack)
.filter(Objects::nonNull)
.map(CustomerTrack::getId)
.flatMap(trackId -> tracks
.stream()
.filter(track -> Long.valueOf(track.getId()).equals(trackId)))
.collect(Collectors.toSet());

Java 8 Stream with map and multiples sets

I am trying to write these lines using java8 streams:
for (Town town : getAllTowns(routes)) {
if (originTown.equals(town))
continue;
for (Route route : routes) {
if (route.hasOrigin(originTown) && route.hasDestine(town)) {
distances.put(town, route.getDistance());
break;
}
distances.put(town, maxDistance);
}
}
return distances; //Map<Town,Integer>
The result that I got so far is:
Map<Town, Integer> distances = getAllTowns(routes).stream()
.filter(town -> !originTown.equals(town))
.forEach(town -> routes.stream()
.filter(route -> route.hasOrigin(originTown) && route.hasDestine(town)
...)
return distances;
How can I collect after the inner filter and build the Map< Town,Integer> where the integer is the route.getDistance()?
I tried to use:
.collect(Collectors.toMap(route -> route.getDestineTown(), route -> route.getDistance()))
But it is inside the forEach call, then I can't return it to my variable distances because it generates the map only for the inner call. I did not understand it. Any input would be really helpful. Thanks.
You can use findFirst() to build a list that contains, for each town, the first route that has that town as the destination, and then call toMap() on it. The default values for missing cities can be handled separately.
Collection<Town> towns = getAllTowns(routes);
Map<Town, Integer> distances = towns.stream()
.filter(town -> !originTown.equals(town))
.map(town -> routes.stream()
.filter(route -> route.hasOrigin(originTown) && route.hasDestine(town))
.findFirst())
.filter(Optional::isPresent)
.collect(toMap(route -> route.get().getDestine(), route -> route.get().getDistance()));
towns.stream()
.filter(town -> !distances.containsKey(town))
.forEach(town -> distances.put(town, maxDistance));
(Note that town is no longer available in collect(), but you can take advantage of the fact that each route got added only if its destination town was town.)
Also note that toMap() doesn't accept duplicate keys. If there can be multiple routes to any town (which I assume there might be), you should use groupingBy() instead.
I think you have two options to solve this. Either you create your resulting Map beforehand and use nested foreachs:
Map<Town, Integer> distances = new HashMap<>();
getAllTowns(routes).stream().filter(town -> !originTown.equals(town))
.forEach(town -> routes.stream().forEach(route -> distances.put(town,
route.hasOrigin(originTown) && route.hasDestine(town) ? route.getDistance() : maxDistance)));
The other option is to collect your stream to a Map by creating an intermediate Object which is essentially a Pair of Town and Integer:
Map<Town, Integer> distances = getAllTowns(routes).stream().filter(town -> !originTown.equals(town))
.flatMap(town -> routes.stream()
.map(route -> new AbstractMap.SimpleEntry<Town, Integer>(town,
route.hasOrigin(originTown) && route.hasDestine(town) ? route.getDistance()
: maxDistance)))
.collect(Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));

Is this the right use of java.util.stream?

Update
Ok, I think I know how to work with streams now. The code in the old post is a mess and I'm not proud of it. So, thank you for you help for directing me in the right direction. I wrote a supplier class, which provides me with items and used static filters and mapper functions:
final TradeSupplier tradeSupplier = new TradeSupplier();
Stream.generate(tradeSupplier).map(TradeSupplier::getPrice)
.map(TradeSupplier::getTradePartner)
.map(TradeSupplier::getTradeInfo)
.filter(TradeSupplier::validateInfo)
.map(TradeSupplier::getPartnerAssetId)
.filter(TradeSupplier::validatePartnerAssetId).forEach(t -> {
if (trade.sendTrade(t)) {
tradeSupplier.finishedItem();
TradeCache.save(t);
}
});
With this design, I don't need flatMap, because it's just an one by one mapping. Additional information is filed into the item, which is just in the stream
I hope, this code is better than the code below... What do you think?
I'm appreciative for any help to improve my understanding of streams :)
Old post
I'm looking for help for the "new" stream api of java 8: first I get a list of items, for every item I collect a list of strings and after that, i want to combine the string with their corresponding item:
input:
item1
item2
wanted output:
item1; string1
item1; string2
item2; string1
item2; string2
item2; string3
Is the following code the right way to use this api?
Code (with stream api)
// input is a list of items
analyst.getInRange(wantMinValue, wantMaxValue)
.stream()
.filter(i -> !haveItem.getName().contains(i.getName())
|| (!haveItem.getModel().contains(i.getModel()) && haveItem
.getQuality() > i.getQuality()))
// get extra information and add it to a list (key: item; value: string)
.collect(Collectors.toMap(s -> s, s -> lounge.getItemId(s)))
.entrySet()
.stream()
// delete all null and empty strings
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
// for every entry in list, create an list and add to result
.forEach(
e -> {
lounge.getListOfValue(e.getValue(), 1)
.stream()
.filter(s -> s != null && !s.isEmpty())
.map(s -> lounge.getStringFromOldString(s))
.filter(s -> s != null && !s.isEmpty())
.collect(
Collectors
.toCollection(HashSet::new))
// add list to resulting list
.forEach(
s -> {
result.add(new TradeLink(s,
haveItem, e.getKey()));
});
});
First thing: .filter(s -> s != null && !s.isEmpty())
Don't include these things unless these are actually things that can happen. Are empty strings or null strings actually going to come up in your application? (If so, that probably reflects a design flaw in the first place. It may be better to let your application crash, because nulls generally shouldn't be in your program in ways like this.)
Second: don't do the mutable thing you're doing here:
.forEach(
e -> {
lounge.getListOfValue(e.getValue(), 1)
.stream()
.filter(s -> s != null && !s.isEmpty())
.map(s -> lounge.getStringFromOldString(s))
.filter(s -> s != null && !s.isEmpty())
.collect(
Collectors
.toCollection(HashSet::new))
// add list to resulting list
.forEach(
s -> {
result.add(new TradeLink(s,
haveItem, e.getKey()));
});
});
Instead, do something like:
.flatMap(e ->
lounge.getListOfValue(e.getValue(), 1)
.stream()
.map(lounge::getStringFromOldString)
.distinct()
.map(s -> new TradeLink(s, haveItem, e.getKey()))
.collect(toList())

Categories