2

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?

2
  • Yes, but it won't be pretty. Commented May 17, 2022 at 16:55
  • Could you please ping a better way solution for this? Best I got has multiple iterations :( Commented May 17, 2022 at 16:57

3 Answers 3

3

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());
Sign up to request clarification or add additional context in comments.

Comments

2

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

5 Comments

flatMapping() is not in Java 8. Then again, neither is List.of().
Nice! But comparator on priority is missing. Possible with flatMapping ?
@Forece85 Updated, the answer. Possible with flatMapping - do you mean is it possible to achieve without flatMapping() or you mean apply sorting within flatMapping()?
@Forece85 Updated with the code fully complaint with Java 8 (without flatMapping()).
@shmosel Correct. I've missed that flatMapping() came with Java 9. It could be replaced with mapping() and collectingAndThen() will deal with flattening sorting. Regarding the usage of List.of() is handy for demo purposes and OP already has the input list, hence it doesn't seem reasonable to me replacing it Arrays.asList() which isn't a better option for latter version of Java.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.