У меня есть список объектов 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?
Не могли бы вы найти лучшее решение для этого? Лучшее, что у меня есть, имеет несколько итераций :(
Вы можете создать промежуточную карту, сгруппировав данные по 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()
.
Ницца! А вот компаратор по приоритету отсутствует. Возможно с помощью flatMapping?
@Forece85 Обновлено, ответ. Возможно с помощью flatMapping - вы имеете в виду, что это возможно без flatMapping()
или вы имеете в виду применить сортировку внутри flatMapping()
?
@ Forece85 Обновлен код, полностью совместимый с Java 8 (без flatMapping()
).
@shmosel Правильно. Я пропустил, что flatMapping()
появился с Java 9. Его можно заменить на mapping()
, а collectingAndThen()
будет иметь дело с сортировкой сглаживания. Что касается использования List.of()
, это удобно для демонстрационных целей, а в OP уже есть список ввода, поэтому мне не кажется разумным заменять его Arrays.asList()
, что не является лучшим вариантом для последней версии Java.
Я бы начал с простой группировки, чтобы получить карту 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());
Имхо, это самый простой способ. Я предполагаю, что у вас есть соответствующие геттеры, определенные для ваших классов.
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(...))
в конструкции вашего списка.
Да, но это будет некрасиво.