Home > OS >  Create a Map of Optionals in Java using Streams
Create a Map of Optionals in Java using Streams

Time:08-30

I need to find a method (using streams) to return a Map<Category,Optional<ToDo>, which help me group an ArrayList and give me a ToDo object with the highest priority of each category.

public record ToDo(String name, Category category, 
                   int priority, LocalDate date) {}
public enum Category { HOME, WORK }

An example of the input data:

List<ToDo> todo = List.of(
    new ToDo("Eat", Category.HOME, 1, LocalDate.of(2022, 8, 29)),
    new ToDo("Sleep", Category.HOME, 2, LocalDate.of(2022, 8, 30)),
    new ToDo("Learn", Category.WORK, 2, LocalDate.of(2022, 9, 3)),
    new ToDo("Work", Category.WORK, 3, LocalDate.of(2022, 10, 3))
);

And in the end, I want to have something like this as a result:

{HOME=[ToDo{Description='Eat', category=HOME, priority=1, deadline=2022-08-29},
 WORK=[ToDo{Description='Learn', category=WORK, priority=2, deadline=2022-09-03]} 

I was trying to use

.collect(Collectors.groupingBy(p -> p.getCategory()));

and

.sorted(Comparator.comparing(ToDo::getPriority)).findFirst();

But I can't do it in a single method and get Optional as a result. How can I resolve this problem?

CodePudding user response:

The practice of storing Optionals in a Collection is discouraged. Apart from being cumbersome because of the need to deal with optional after retrieving value a Map, it goes against the design goal of Optional which is intended to be used as a return type.

And while using collector groupingBy() it's always guaranteed that a ToDo object mapped to a particular Category will always be present.

To find a ToDo with the highest priority in a particular category, you can use collector maxBy(), which expects a Comparator, as a downstream collector of groupingBy(). It would be way more efficient than combination .sorted().findFirst().

List<ToDo> todo = List.of(
    new ToDo("Eat", Category.HOME, 1, LocalDate.of(2022, 8, 29)),
    new ToDo("Sleep", Category.HOME, 2, LocalDate.of(2022, 8, 30)),
    new ToDo("Learn", Category.WORK, 2, LocalDate.of(2022, 9, 3)),
    new ToDo("Work", Category.WORK, 3, LocalDate.of(2022, 10, 3))
);

Map<Category, ToDo> highestPriorityTaskByCategory = todo.stream()
    .collect(Collectors.groupingBy(
        ToDo::category,
        Collectors.collectingAndThen(
            Collectors.maxBy(Comparator.comparingInt(ToDo::priority)),
            Optional::orElseThrow)
    ));

highestPriorityTaskByCategory.forEach((k, v) -> System.out.println(k   " -> "   v));

Output:

HOME -> ToDo[name=Sleep, category=HOME, priority=2, date=2022-08-30]
WORK -> ToDo[name=Work, category=WORK, priority=3, date=2022-10-03]

A link to Online Demo

CodePudding user response:

I assume that the DOM should be HOME, because that's the category inside your object. I also assume that the ToDoList should be an Optional<ToDo>.

The groupingBy you used will return a Map<Category, List<ToDo>>. That's the right key but the wrong value. You need to solve that by also supplying a downstream collector, that will collect ToDo elements:

Map<Category, Optional<ToDo>> result = todo.stream()
        .collect(Collectors.groupingBy(
                ToDo::getCategory,
                Collectors.minBy(Comparator.comparingInt(ToDo::getPriority))
        ));

You can improve the comparator to Comparator.comparingInt(ToDo::getPriority).thenComparing(ToDo::getDeadline) to find the entry with the earliest deadline in case some have the same lowest priority.

Note that this will only include entries for categories that are part of your input. If you need all, use an additional loop:

for (Category category : Category.values()) {
    result.computeIfAbsent(category, c -> Optional.empty());
}
  • Related