Преобразовать карту <String, Object> в карту <String, Set <Object >> с фильтром и потоками

Я хотел бы преобразовать мою карту, которая выглядит следующим образом:

{
  key="someKey1", value=Apple(id="1", color="green"),
  key="someKey2", value=Apple(id="2", color="red"),
  key="someKey3", value=Apple(id="3", color="green"),
  key="someKey4", value=Apple(id="4", color="red"),
}

на другую карту, которая помещает все яблоки одного цвета в один и тот же список:

{
  key="red", value=list={apple1, apple3},
  key="green", value=list={apple2, apple4},  
}

Я попробовал следующее:

Map<String, Set<Apple>> sortedApples = appleMap.entrySet()
    .stream()
    .collect(Collectors.toMap(l -> l.getColour, ???));

Я на правильном пути? Должен ли я использовать фильтры для этой задачи? Есть ли более простой способ?

Ответ 1

если вы хотите продолжить работу с toMap вы можете получить результат следующим образом:

map.values()  // get the apples
   .stream() // Stream<Apple>
   .collect(toMap(Apple::getColour, // group by colour
             v ->  new HashSet<>(singleton(v)), // have values as set of apples
          (l, r) -> {l.addAll(r); return l;})); // merge colliding apples by colour
  • поток по values карты вместо entrySet потому что нас не интересуют ключи карты.
  • Apple::getColour - это функция keyMapper используемая для извлечения "вещи", которую мы хотим сгруппировать, в данном случае по цвету Apple.
  • v → new HashSet<>(singleton(v)) - функция valueMapper используемая для результирующих значений карты
  • (l, r) → {l.addAll(r); return l;} (l, r) → {l.addAll(r); return l;} - это функция слияния, используемая для объединения двух HashSet когда происходит коллизия клавиш в цвете Apple.
  • наконец, итоговая карта представляет собой Map<String, Set<Apple>>

но это лучше с groupingBy и toSet как downstream:

map.values().stream().collect(groupingBy(Apple::getColour, toSet()));
  • поток по values карты вместо entrySet потому что нас не интересуют ключи карты.

  • группирует Apple по предоставленной функции классификации, то есть Apple::getColour а затем собирает значения в наборе, следовательно, в нисходящий коллектор toSet.

  • наконец, итоговая карта представляет собой Map<String, Set<Apple>>

краткий, читаемый и идиоматический подход.

Вы также можете сделать это без потока:

Map<String, Set<Apple>> res = new HashMap<>();
map.values().forEach(a -> res.computeIfAbsent(a.getColour(), e -> new HashSet<>()).add(a));
  • итерируйте по values карты вместо entrySet потому что нас не интересуют ключи карты.
  • если указанный ключ a.getColour() еще не связан со значением, он пытается вычислить его значение, используя заданную функцию отображения e → new HashSet<>() и вводит его в карту. Затем мы добавляем Apple в результирующий набор.
  • если указанный ключ a.getColour() уже связан со значением, computeIfAbsent возвращает существующее значение, связанное с ним, и затем мы вызываем add(a) в HashSet чтобы ввести Apple в набор.
  • наконец, итоговая карта представляет собой Map<String, Set<Apple>>

Ответ 2

Collectors.groupingBy более подходит для этой задачи, чем Collectors.toMap (хотя можно использовать и то, и другое).

Map<String, List<Apple>> sortedApples = 
    appleMap.values()
            .stream()
            .collect(Collectors.groupingBy(Apple::getColour));

Или сгруппировать их в Set:

Map<String, Set<Apple>> sortedApples = 
    appleMap.values()
            .stream()
            .collect(Collectors.groupingBy(Apple::getColour,
                                           Collectors.mapping(Function.identity(),
                                                              Collectors.toSet())));

или (как прокомментировал Аомин):

Map<String, Set<Apple>> sortedApples = 
    appleMap.values()
            .stream()
            .collect(Collectors.groupingBy(Apple::getColour, Collectors.toSet()));

Ответ 3

Вы можете использовать Collectors.groupingBy и Collectors.toSet()

Map<String, Set<Apple>> sortedApples = appleMap.values() // Collection<Apple>
        .stream() // Stream<Apple>
        .collect(Collectors.groupingBy(Apple::getColour, // groupBy colour
                Collectors.mapping(a -> a, Collectors.toSet()))); // collect to Set

Ответ 4

Вы спросили, как сделать это с потоками, но здесь есть другой способ:

Map<String, Set<Apple>> result = new LinkedHashMap<>();
appleMap.values().forEach(apple -> 
    result.computeIfAbsent(apple.getColor(), k -> new LinkedHashSet<>()).add(apple));

При этом используется Map.computeIfAbsent, который либо возвращает набор, сопоставленный этому цвету, либо помещает пустой LinkedHashSet в карту, если еще ничего не сопоставлено с этим цветом, затем добавляет яблоко в набор.

РЕДАКТИРОВАТЬ: я использую LinkedHashMap и LinkedHashSet чтобы сохранить порядок вставки, но мог бы использовать HashMap и HashSet, соответственно.