Home > Net >  Ruducing a List of objects using Stream.reduce()
Ruducing a List of objects using Stream.reduce()

Time:05-24

I have a list of objects, and I need to group objects having status equal to my customizedStatusto 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 ?

CodePudding user response:

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