Группировка и сортировка вложенных объектов с использованием Java Streams

У меня есть список объектов DTO с полем вложенного списка.

Цель состоит в том, чтобы группа их по полю id и сливаться, а затем Сортировать по списку с помощью 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)))
);

Мне нужно группа эти вложенные объекты с одним и тем же полем id и сливаться все элементы в в порядке убывания по полю priority.

Окончательный результат для этого dtoList будет примерно таким:

// 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),
    ];

Можем ли мы добиться этого с помощью Streams API?

Да, но это будет некрасиво.

shmosel 17.05.2022 18:55

Не могли бы вы найти лучшее решение для этого? Лучшее, что у меня есть, имеет несколько итераций :(

Forece85 17.05.2022 18:57
Основы программирования на Java
Основы программирования на Java
Java - это высокоуровневый объектно-ориентированный язык программирования, основанный на классах.
Концепции JavaScript, которые вы должны знать как JS программист!
Концепции JavaScript, которые вы должны знать как JS программист!
JavaScript (Js) - это язык программирования, объединяющий HTML и CSS с одной из основных технологий Всемирной паутины. Более 97% веб-сайтов используют...
2
2
58
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете создать промежуточную карту, сгруппировав данные по id, а затем преобразовав каждую запись в новый DTO объект.

Для этого вы можете использовать комбинацию встроенных сборщиков groupingBy() и flatMapping() для создания промежуточной карты.

Чтобы отсортировать элементы, сопоставленные каждому id, flatMapping() используется вместе с 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);
}

Выход

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}]}

Как отметил @shmosel, flatMapping() — одно из преимуществ Java 9. Вы также можете думать об этом как о напоминании, возможно, пришло время перейти к модульной системе, предоставляемой Java 9, и другим полезным функциям.

Версия, полностью соответствующая Ява 8, будет выглядеть так:

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());
flatMapping() нет в Java 8. Опять же, нет и List.of().
shmosel 17.05.2022 19:08

Ницца! А вот компаратор по приоритету отсутствует. Возможно с помощью flatMapping?

Forece85 17.05.2022 19:16

@Forece85 Обновлено, ответ. Возможно с помощью flatMapping - вы имеете в виду, что это возможно без flatMapping() или вы имеете в виду применить сортировку внутри flatMapping()?

Alexander Ivanchenko 17.05.2022 19:20

@ Forece85 Обновлен код, полностью совместимый с Java 8 (без flatMapping()).

Alexander Ivanchenko 17.05.2022 19:35

@shmosel Правильно. Я пропустил, что flatMapping() появился с Java 9. Его можно заменить на mapping(), а collectingAndThen() будет иметь дело с сортировкой сглаживания. Что касается использования List.of(), это удобно для демонстрационных целей, а в OP уже есть список ввода, поэтому мне не кажется разумным заменять его Arrays.asList(), что не является лучшим вариантом для последней версии Java.

Alexander Ivanchenko 17.05.2022 20:22
Ответ принят как подходящий

Я бы начал с простой группировки, чтобы получить карту Map<Long,List<DTO>>, пропустить записи этой карты и сопоставить каждую с новым DTO. Вы можете извлечь метод/функцию для сортировки ItemDTO:

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

Имхо, это самый простой способ. Я предполагаю, что у вас есть соответствующие геттеры, определенные для ваших классов.

  • просто скрыто на карту, указанную на идентификаторе.
  • объединить соответствующие списки
  • и вернуть значения и преобразовать в 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());

Затем просто отсортируйте их на месте в соответствии с вашими требованиями. Это занимает не больше времени, чем в конструкции потока, но, на мой взгляд, менее загромождено.

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

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

печатает (используя простой toString для двух классов)

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

Примечание: List.of неизменяем, поэтому вы не можете их изменить. Я бы использовал new ArrayList<>(List.of(...)) в конструкции вашего списка.

Другие вопросы по теме