Java 8 Streams - Call "map" during stream operations without saving to variable - java

I could not find many answers to this, though I suppose its not possible, but I was curious - is there anyway in Java8 streams to "refer" or to "call" the HashMap that is created via the .collectors(Collectors.toMap(key -> "key", val -> "val")? I know I can save this resultant reduction which creates the Map to Map<String, String> myMap variable, but I still had to perform operations on this resultant HashMap and I wanted to keep the stream going, so I wanted to do something like this:
myHashSet.stream().
.filter(i -> i != null)
.collectors(Collectors.toMap(key -> "key", val -> "val")
.forEach((k,v) -> {
// HERE IS WHERE I WANT TO CALL THE MAP CREATED ABOVE
if( MAP.contains("someRandomValue") {
}
}));
I'm assuming you can't do this, but I was hoping there was some method or something so I don't have to "kill" the stream, i.e. save the Map to a variable, then proceed to stream it out again etc...

The collect operation in Stream is a terminal operation. So there is no Stream after collect. You would have to do something like below.
var myMap = myHashSet.stream().
.filter(i -> i != null)
.collectors(Collectors.toMap(key -> "key", val -> "val");
myMap.forEach((k,v) -> {
// HERE IS WHERE I WANT TO CALL THE MAP CREATED ABOVE
if( MAP.contains("someRandomValue") {
}
}));
Below article describes about the available intermediate operations in Stream.
https://www.javacodegeeks.com/2020/04/java-8-stream-intermediate-operations-methods-examples.html

I solved this with #shmosel's help using collectingAndThen. This was the closest to what I wanted to do.
myArrayList.stream().
.collect(Collectors.collectingAndThen(Collectors.toMap(key -> key.getName(), val -> val), map -> map.forEach((key, val) -> System.out.println(map.contains(key)));

Related

Using Java Optional within stream mapping

I have code like this:
public void processList(List<String> list) {
for (String item : list) {
Object obj = getObjectForString(item);
if (obj != null) {
doSomethingWithObject(obj);
} else {
System.err.println("Object was null for " + item);
}
}
}
Ideally I would like to streamline this and avoid the null check using list.stream().map( *blah, blah, blah* ), and doSomethingWithObject if the object is not null, but log the error otherwise (by using the orElse method on an optional). I'm not super savvy with this Java 8 functionality and not sure if there is a nice, slick way to do what I want here or not. Suggestions?
Edit to add a failed attempt at this:
list.stream()
.map(p -> getObjectForString(p))
.map(Optional::ofNullable)
.forEach(
p -> p.ifPresentOrElse(
r -> doSomethingWithObject(r),
() -> System.err.println("Object was null")
));
Even if that code behaved the way I want, it still doesn't append the String from the original list to the error message as I would like it to. But maybe that's too much complexity to try to accomplish with streams like this.
we should propagate the item even after conversion. The slick way is using tuple or pair.
I used Tuple from vavr functional library to do the same. And below is the code for your reference
list.stream()
.map(p -> Tuple.of(p, getObjectForString(p)).map2(Optional::ofNullable))
.forEach(p -> p._2.ifPresentOrElse(
r -> doSomethingWithObject(r),
() -> System.err.println("Object was null" + p._1))
);
Another approach would be to collect the items in to separate 2 buckets/partitions based on if the item had an associated object or not. After that, process the 2 buckets as required:
final Boolean HAS_OBJECT = Boolean.FALSE;
Map<Boolean, List<String>> partitionedMap = list.stream()
.collect(Collectors.partitioningBy(item -> !Objects.isNull(getObjectForString(item))));
partitionedMap.get(HAS_OBJECT).stream()
.map(item -> getObjectForString(item))
.forEach(obj -> doSomethingWithObject(obj));
partitionedMap.get(!HAS_OBJECT)
.forEach(item -> System.err.println("Object was null for " + item));
Even though the below method does not avoid a null check as you wanted in your question, this is just another way to achieve the same result. (Only benefit is that it saves 1-2 lines of code!).
The below code uses Runnable (takes no arguments and returns nothing as well) along with Java 8's Function.
NOTE : I would still recommend the normal for loop :-), as I believe that the below might look fancy, but the for loop is more easy to understand in this particular case.
Function<String, Runnable> func = item -> {
Object obj = getObjectForString(item);
return (obj != null) ? ( () -> doSomethingWithObject(obj))
: ( () -> System.err.println("Object was null for " + item));
};
list.stream().map(func).forEach(Runnable::run);

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

Streams on nested map

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

Group, Collectors, Map (Int to String), Map (Map to Object)

This is a continuation of my previous question at Group, Sum byType then get diff using Java streams.
As suggested, I should post as a separate thread instead of updating the original one.
So with my previous set of question, I have achieved that, and now, with the continuation.
Background:
I have the following dataset
Sample(SampleId=1, SampleTypeId=1, SampleQuantity=5, SampleType=ADD),
Sample(SampleId=2, SampleTypeId=1, SampleQuantity=15, SampleType=ADD),
Sample(SampleId=3, SampleTypeId=1, SampleQuantity=25, SampleType=ADD),
Sample(SampleId=4, SampleTypeId=1, SampleQuantity=5, SampleType=SUBTRACT),
Sample(SampleId=5, SampleTypeId=1, SampleQuantity=25, SampleType=SUBTRACT)
Sample(SampleId=6, SampleTypeId=2, SampleQuantity=10, SampleType=ADD),
Sample(SampleId=7, SampleTypeId=2, SampleQuantity=20, SampleType=ADD),
Sample(SampleId=8, SampleTypeId=2, SampleQuantity=30, SampleType=ADD),
Sample(SampleId=9, SampleTypeId=2, SampleQuantity=15, SampleType=SUBTRACT),
Sample(SampleId=10, SampleTypeId=2, SampleQuantity=35, SampleType=SUBTRACT)
I am currently using this:
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getTypeId,
Collectors.summingInt(
sample -> SampleType.ADD.equalsIgnoreCase(sample.getSampleType())
? sample.getSampleQuantity() :
-sample.getSampleQuantity()
)));
And also this
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getSampleTypeId,
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
as the accepted answer to get the desired output to group in a Map<Long, Integer>:
{1=15, 2=10}
With that, I was wondering, if this could be expanded into something more.
First, how could I have it return as a Map<String, Integer> instead of the original Map<Long, Integer>. Basically, for the SampleTypeId; 1 refers to HELLO, 2 refers to WORLD.
So I would need like a .map (or maybe other function) to transform the data from 1 to HELLO and 2 to WORLD by calling a function say convertType(sampleTypeId)?. So the expected output would then be {"HELLO"=15, "WORLD"=10}. Is that right? How should I edit the current suggested solution to this?
Lastly, I would like to know if it is also possible to return it to a Object instead of a Map. So let's say I have a Object; SummaryResult with (String) name and (int) result. So it returns a List<SummaryResult> instead of the original Map<Long, Integer>. How can I use the .map (or other) feature to do this? Or is there other way to doing so? The expected output would be something along this line.
SummaryResult(name="hello", result=15),
SummaryResult(name="world", result=10),
Would really appreciate it with the explanation in steps as given previously by #M. Prokhorov.
Update:
After updating to
sampleList.stream()
.collect(Collectors.groupingBy(sample -> convertType(sample.getSampleTypeId()),
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
private String convertType(int id) {
return (id == 1) ? "HELLO" : "WORLD";
}
For first part, considering you have somewhere the method
String convertType(int typeId)
You simply need to change first classifier from this
groupingBy(SampleType::getTypeId)
to this
groupingBy(sample -> convertType(sample.getTypeId()))
Everything else remains the same.
Latter type is a little trickier, and technically doesn't benefit from it being a stream-related solution at all.
What you need is this:
public List<SummaryResult> toSummaryResultList(Map<String, Integer> resultMap) {
List<SummaryResult> list = new ArrayList<>(resultMap.size());
for (Map.Entry<String, Integer> entry : resultMap.entrySet()) {
String name = entry.getKey();
Integer value = entry.getValue();
// replace below with construction method you actually have
list.add(SummaryResult.withName(name).andResult(value));
}
return list;
}
You can use this as part of collector composition, where your whole collector will get wrapped into a collectingAndThen call:
collectingAndThen(
groupingBy(sample -> convertType(sample.getTypeId()),
collectingAndThen(
groupingBy(Sample::getSampleType,
summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))),
result -> toSummaryResultList(result))
However, as you can see, it is the whole collector that gets wrapped, so there is no real benefit in my eyes to the above version to a simpler and easier to follow (at least to me) version below that uses an intermediate variable, but isn't so much of a wall of code:
// do the whole collecting thing like before
Map<String, Integer> map = sampleList.stream()
.collect(Collectors.groupingBy(sample -> convertType(sample.getTypeId()),
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))));
// return the "beautified" result
return toSummaryResultList(map);
Another point to consider in above is: convertType method will be called as many times as there are elements in sampleList, so if convertType call is "heavy" (for example, uses database or IO), then it's better to call it as part of toSummaryResultList conversion, not as stream element classifier. In which case you will be collecting from map of type Map<Integer, Integer> still, and using convertType inside the loop. I will not add any code with this in consideration, as I view this change as trivial.
You could indeed use a map() function
sampleList.stream()
.collect(Collectors.groupingBy(Sample::getSampleTypeId,
Collectors.collectingAndThen(
Collectors.groupingBy(Sample::getSampleType,
Collectors.summingInt(Sample::getSampleQuantity)),
map -> map.getOrDefault(SampleType.ADD, 0)
- map.getOrDefault(SampleType.SUBTRACT, 0))))
.entrySet()
.stream()
.map(entry->new SummaryResult(entry.getKey()),entry.getValue())
.collect(Collectors.toList());
ToIntFunction<Sample> signedQuantityMapper= sample -> sample.getQuantity()
* (sample.getType() == Type.ADD ? 1 : -1);
Function<Sample, String> keyMapper = s -> Integer.toString(s.getTypeId());
Map<String, Integer> result = sampleList.stream().collect(
Collectors.groupingBy(
keyMapper,
Collectors.summingInt(signedQuantityMapper)));

Nested for each loop returning map with Java 8 streams

I just started with Java 8 and streams, and not able to find out how to write this code in Java 8:
Map<Integer, CarShop> result = new HashMap<>();
for (Car car : someListOfCars) {
List<CarProduct> listOfCarProducts = car.getCarProducts();
for (CarProduct product : listOfCarProducts) {
result.put(product.getId(), car.getCarShop());
}
}
Any help?
You can often convert your iterative solution directly to a stream by using .collect:
Map<Integer, CarShop> result = someListOfCars.stream().collect(
HashMap::new,
(map, car) -> car.getCarProducts().forEach(
prod -> map.put(prod.getId(), car.getCarShop())
),
Map::putAll
);
You can make the solution more flexible at the cost of additional allocations:
Map<Integer, CarShop> result = someListOfCars.stream()
.flatMap(car -> car.getCarProducts().stream()
.map(prod -> new SimpleImmutableEntry<>(prod.getId(), car.getCarShop()))
).collect(toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> b));
This will allow you to collect any way you wish. For example, you would be able to remove (a,b)->b to force an exception if there are duplicate ids instead of silently overwriting the entry.

Categories