Home > Back-end >  Group and Sort nested Objects using Java Streams
Group and Sort nested Objects using Java Streams

Time:05-18

I have a list of DTO objects with the nested list field.

The aim is to group them by id field and merge, and then sort the list using Streams API.

class DTO {
    private Long id;
    private List<ItemDTO> items;
}

class ItemDTO {
    private Long priority;
    private Long value;
}

// input
List<DTO> dtoList = List.of(
 DTO(1, List.of(ItemDTO(1, 1), ItemDTO(7, 2))),
 DTO(2, List.of(ItemDTO(1, 1), ItemDTO(2, 2))),
 DTO(1, List.of(ItemDTO(10, 3), ItemDTO(1, 4)))
);

I need to group these nested objects with the same id field and merge all items in descending order by field priority.

The final result for this dtoList will be something like this:

// output 
List<DTO> resultList = [
        DTO(1, List.of(ItemDTO(10,3), ItemDTO(7,2), ItemDTO(1,1), ItemDTO(1,4)),
        DTO(2, List.of(ItemDTO(2,2), ItemDTO(1,1),
    ];

Can we achieve this with Streams API?

CodePudding user response:

I would start by a simple grouping by to get a map Map<Long,List<DTO>> and stream over the entries of that map and map each to a new DTO. You can extract a method / function to get the ItemDTOs sorted:

import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

....


Function<List<DTO>, List<ItemDTO>> func =
        list -> list.stream()
                .map(DTO::getItems)
                .flatMap(List::stream)
                .sorted(Comparator.comparing(ItemDTO::getPriority,Comparator.reverseOrder()))
                .collect(Collectors.toList());

List<DTO> result = 
        dtoList.stream()
               .collect(Collectors.groupingBy(DTO::getId))
               .entrySet().stream()
               .map(entry -> new DTO(entry.getKey(), func.apply(entry.getValue())))
               //.sorted(Comparator.comparingLong(DTO::getId)) if the resulting list need to be sorted by id
               .collect(Collectors.toList());

CodePudding user response:

You can create an intermediate map by grouping the data by id and then transform each entry into a new DTO object.

For that, you can use a combination of built-in collectors groupingBy() and flatMapping() to create an intermediate map.

In order to sort the items mapped each id, flatMapping() is being used in conjunction with collectionAndThen().

public static void main(String[] args) {
    // input
    List<DTO> dtoList = List.of(
        new DTO(1L, List.of(new ItemDTO(1L, 1L), new ItemDTO(7L, 2L))),
        new DTO(2L, List.of(new ItemDTO(1L, 1L), new ItemDTO(2L, 2L))),
        new DTO(1L, List.of(new ItemDTO(10L, 3L), new ItemDTO(1L, 4L)))
    );
    
    List<DTO> result = dtoList.stream()
        .collect(Collectors.groupingBy(DTO::getId,
            Collectors.collectingAndThen(
            Collectors.flatMapping(dto -> dto.getItems().stream(), Collectors.toList()),
                (List<ItemDTO> items) -> {
                    items.sort(Comparator.comparing(ItemDTO::getPriority).reversed());
                    return items;
            })))
        .entrySet().stream()
        .map(entry -> new DTO(entry.getKey(), entry.getValue()))
        .collect(Collectors.toList());
    
    result.forEach(System.out::println);
}

Output

DTO{id = 1, items = [ItemDTO{10, 3}, ItemDTO{7, 2}, ItemDTO{1, 1}, ItemDTO{1, 4}]}
DTO{id = 2, items = [ItemDTO{2, 2}, ItemDTO{1, 1}]}

As @shmosel has pointed out, flatMapping() is one of the boons of Java 9. You may also think of it as a reminder, maybe it's time to move to the modular system provided by Java 9 and other useful features.

The version that is fully compliant with Java 8 will look like this:

List<DTO> result = dtoList.stream()
    .collect(Collectors.groupingBy(DTO::getId,
        Collectors.collectingAndThen(
            Collectors.mapping(DTO::getItems, Collectors.toList()),
                (List<List<ItemDTO>> items) ->
                    items.stream().flatMap(List::stream)
                        .sorted(Comparator.comparing(ItemDTO::getPriority).reversed())
                        .collect(Collectors.toList())
                    )))
            .entrySet().stream()
            .map(entry -> new DTO(entry.getKey(), entry.getValue()))
            .collect(Collectors.toList());

CodePudding user response:

Imo, this is the easiest way. I am presuming you had the appropriate getters defined for you classes.

  • simply covert to a map keyed on the id.
  • merge the appropriate lists
  • and return the values and convert to an ArrayList.
List<DTO> results = new ArrayList<>(dtoList.stream().collect(
        Collectors.toMap(DTO::getId, dto -> dto, (a, b) -> {
            a.getItems().addAll(b.getItems());
            return a;
        })).values());

Then simply sort them in place based on your requirements. This takes no more time that doing it in the stream construct but in my opinion is less cluttered.

for (DTO d: results) {   
    d.getItems().sort(Comparator.comparing(ItemDTO::getPriority)
       .reversed());
}

results.forEach(System.out::println);

prints (using a simple toString for the two classes)

DTO[1, [ItemDTO[10, 3], ItemDTO[7, 2], ItemDTO[1, 1], ItemDTO[1, 4]]]
DTO[2, [ItemDTO[2, 2], ItemDTO[1, 1]]]

Note: List.of is immutable so you can't change them. I would use new ArrayList<>(List.of(...)) in your list construct.

  • Related