I have a list of objects, and I need to group objects having status
equal to my customizedStatus
to a single customized one with count
= sumOfSameObjectsCount
.
We have class MyObject
class MyObject { Integer id; String name; String status; Long count; //constructor with attributes //getters //setters }
Suggested implementation :
List<MyObject> resultList = listOfObjects.stream() .collect(Collectors.groupingBy(MyObject::getStatus)) .entrySet().stream() .map(e -> e.getValue().stream() .reduce((partialResult,nextElem) -> { LOGGER.info("ahaaaa! inside your reduce block "); if(partialResult.getStatus().equals(customizedStatus)) { LOGGER.info("equal to my customizedStatus"); return new MyObject(customizedId, customizedName, customizedStatus, partialResult.getCount()+nextElem.getCount()); } else { LOGGER.info("not equal to my customizedStatus"); return new MyObject(partialResult.getId(), partialResult.getName(), partialResult.getStatus(), partialResult.getCount()); } } ) ) .map(f -> f.get()) .collect(Collectors.toList());
Things work like a charm in case there are multiple objects with status
equal to my customizedStatus
.
Input :
[ { "id": XX, "name": "nameXX", "status": "statusXX", "count": countXX }, { "id": YY, "name": "nameYY", "status": "statusYY", "count": countYY }, { "id": ZZ, "name": "nameZZ", "status": "customizedStatus", "count": countZZ }, { "id": ZZz, "name": "nameZZz", "status": "customizedStatus", "count": countZZz } ]
Output :
[ { "id": XX, "name": "nameXX", "status": "statusXX", "count": countXX }, { "id": YY, "name": "nameYY", "status": "statusYY", "count": countYY }, { "id": customizedId, "name": "customizedName", "status": "customizedStatus", "count": countZZ+countZZz } ]
In case there is one object with status
equal to my customizedStatus
, need to be customized it too, unfortunately reduce block is being skipped !
Input :
[ { "id": XX, "name": "nameXX", "status": "statusXX", "count": countXX }, { "id": YY, "name": "nameYY", "status": "statusYY", "count": countYY }, { "id": ZZ, "name": "nameZZ", "status": "customizedStatus", "count": countZZ } ]
Output :
[ { "id": XX, "name": "nameXX", "status": "statusXX", "count": countXX }, { "id": YY, "name": "nameYY", "status": "statusYY", "count": countYY }, { "id": ZZ, "name": "nameZZ", "status": "customizedStatus", "count": countZZ } ]
Expected output :
[ { "id": XX, "name": "nameXX", "status": "statusXX", "count": countXX }, { "id": YY, "name": "nameYY", "status": "statusYY", "count": countYY }, { "id": customizedId, "name": "customizedName", "status": "customizedStatus", "count": countZZ } ]
It seems like reduce is executed in case there is multiple objects with same status
, if there isn’t reduce not being executed at all !
Any thoughts to get the expected output using groupBy
and reduce
?
Advertisement
Answer
Update
The resulting type is not correct. Because you didn’t provide the identity within the reduce()
it will return an Optional<Object>
, but not an object.
For the same reason (because you are using a flavor of reduce()
that doesn’t expect identity), the accumulator will have no impact on a single element. A quote from the documentation:
Performs a reduction on the elements of this stream, using an associative accumulation function, and returns an Optional describing the reduced value, if any. This is equivalent to:
boolean foundAny = false; T result = null; for (T element : this stream) { if (!foundAny) { foundAny = true; result = element; } else result = accumulator.apply(result, element); } return foundAny ? Optional.of(result) : Optional.empty();
The first encountered stream element would become a partial result and there’s no more elements, it would be wrapped by the optional as is and returned.
A possible remedy is to introduce the identity:
public static final Integer customizedId = 99; public static final String customizedName = "customizedName"; public static final String customizedStatus = "customizedStatus"; public static void main(String[] args) { List<MyObject> listOfObjects = List.of(new MyObject(1, "nameXX", "statusXX", 1L), new MyObject(2, "nameYY", "statusYY", 1L), new MyObject(3, "nameZZz", "customizedStatus", 3L)); List<MyObject> result = listOfObjects.stream() .collect(Collectors.groupingBy(MyObject::getStatus)) .entrySet().stream() .map(e -> e.getValue().stream() .reduce(getIdentity(e), (partialResult, nextElem) -> accumulate(partialResult, nextElem)) ) .collect(Collectors.toList()); result.forEach(System.out::println); } public static MyObject getIdentity(Map.Entry<String, List<MyObject>> entry) { return entry.getKey().equals(customizedStatus) ? new MyObject(customizedId, customizedName, customizedStatus, 0L) : entry.getValue().iterator().next(); } public static MyObject accumulate(MyObject result, MyObject next) { return result.getStatus().equals(customizedStatus) ? new MyObject(customizedId, customizedName, customizedStatus, result.getCount() + next.getCount()) : new MyObject(result.getId(), result.getName(), result.getStatus(), result.getCount()); }
Output:
MyObject{id=2, name='nameYY', status='statusYY', count=1} MyObject{id=1, name='nameXX', status='statusXX', count=1} MyObject{id=99, name='customizedName', status='customizedStatus', count=3}
You can play around with this Online demo
But keep in mind that it’s not the brightest idea to try to crap a lot of conditional logic into stream because it becomes more difficult to read.
Solutions provided below were written before the question was updated, and the problem was clarified. Although, they don’t target this specific problem, someone might benefit from them and for that reason I’ll preserve them.
Reducing the list into a single object
Is there any solution to make it pass by reduce even listOfObjects entries are different by status ?
In case if you want to reduce a list of objects into a single object with a predefined id
, name
and status
, there’s no need to create an intermediate map with Collectors.groupingBy()
.
If you want to utilize reduce()
operation for that, you can accumulate count
and then create a resulting object based on it:
That’s how it might look like (the type of dummy object was changed to MyObject
to avoid confusion with java.lang.Object
):
final Integer customizedId = // intializing the resulting id final String customizedName = // intializing the resulting name final String customizedStatus = // intializing the resulting status List<MyObject> listOfObjects = // intializing the source list MyObject resultingObject = listOfObjects.stream() .map(MyObject::getCount) .reduce(Long::sum) .map(count -> new MyObject(customizedId, customizedName, customizedStatus, 0L)) .orElseThrow(); // or .orElse(() -> new MyObject(customizedId, customizedName, customizedStatus, 0L));
Another way of achieving it is to make use of the fact that MyObject
is mutable and utilize it as a container inside the collect()
operation:
MyObject resultingObject = listOfObjects.stream() .collect(() -> new MyObject(customizedId, customizedName, customizedStatus, 0L), (MyObject result, MyObject next) -> result.setCount(result.getCount() + next.getCount()), (left, right) -> left.setCount(left.getCount() + right.getCount()));