Карта потока Java на карте списка

#java #java-stream

Вопрос:

У меня есть сущность, которая выглядит так:

 public class Snippet {
    private Integer docId;
    private Integer page;
    private Payload payload;
}
 

Входные данные представляют собой List<Snippet>

Мне нужно создать индекс, который позволит нам перебирать идентификаторы документов и страницы и получать связанные объекты фрагментов.

Итак, структура данных, подобная этой:

Map<Integer, Map<Integer, List<Snippet>>>

Я могу использовать потоки Java для получения карты<Целое число, Карта<Целое число, Фрагмент><Целое число, фрагмент>> — но это не приведет к сбору списка фрагментов в конце.

         List<Snippet> input = ....;
        input.stream()
        .collect(Collectors.groupingBy(
                    Snippet::getDocId, 
                    Collectors.toMap(Snippet::getPage, Function.identity())
                 )
        );
 

Как я могу собрать данные, чтобы получить список в качестве конечного значения карты?

Ответ №1:

Как вы можете видеть, Collectors.groupingBy(..) получает коллектор в качестве 2-го аргумента. Итак, просто используйте Collectors.groupingBy(..) в качестве 2-го аргумента Collectors.groupingBy(..)

 input.stream().collect(Collectors.groupingBy(
                        Snippet::getDocId,
                        Collectors.groupingBy(Snippet::getPage,
                                Collectors.toList())));
 

Комментарии:

1. Спасибо — теперь, когда я это прочитал, это кажется таким очевидным. Я думаю, что я зациклился на необходимости собрать карту в карту — но я думаю, что коллектор groupingBy сам создает карту.

Ответ №2:

Вот еще одна альтернатива для вашего текущего или будущего рассмотрения. Он использует computeIfAbsent метод интерфейса карты.

  • создайте окончательную карту содержимого.
  • выполните итерацию по списку Snippets
  • если DocId этого нет, создайте запись с DocId ключом as. Затем попробуйте сохранить и page Snippet
  • если page этого нет, создайте запись с page ключом as. Затем сохраните Snippet его в списке.

Вот пример использования записей вместо классов.

 record PayLoad(String getValue) {
    @Override
    public String toString() {
        return "["   getValue()   "]";
    }
}

record Snippet(int getDocId, int getPage,
        PayLoad getPayLoad) {
    @Override
    public String toString() {
        return String.format("{%s, %s, %s}", getDocId,
                getPage, getPayLoad);
    }
}
 

Создайте некоторые данные

 List<Snippet> input =
        List.of(new Snippet(1, 1, new PayLoad("A")),
                new Snippet(1, 2, new PayLoad("B")),
                new Snippet(1, 3, new PayLoad("C")),
                new Snippet(2, 1, new PayLoad("D")),
                new Snippet(2, 2, new PayLoad("E")),
                new Snippet(2, 3, new PayLoad("F")));
 

Сохраните фрагменты

 Map<Integer, Map<Integer, List<Snippet>>> map =
        new HashMap<>();

for (Snippet s : input) {
    map.computeIfAbsent(s.getDocId(), v -> new HashMap<>())
            .computeIfAbsent(s.getPage(),
                    v -> new ArrayList<>())
            .add(s);
}
 

Распечатайте их

 map.forEach((k, v) -> {
    System.out.println(k);
    v.forEach((kk, vv) -> System.out
            .println("          "   kk   " -> "   vv));
});
 

С принтами

 1
          1 -> [{1, 1, [A]}]
          2 -> [{1, 2, [B]}]
          3 -> [{1, 3, [C]}]
2
          1 -> [{2, 1, [D]}]
          2 -> [{2, 2, [E]}]
          3 -> [{2, 3, [F]}]
 

Комментарии:

1. Минимальная версия Java 17?

2. Нисколько. Записи были сделаны просто для упрощения примера, но были доработаны в Java 16. Карта является Java 8, поэтому фактический производственный запуск будет работать с Java 8 .

3. Это очень умное решение — спасибо. computeIfAbsent создает очень элегантный код для случаев, которые слишком сложны для потоковой операции. Спасибо вам за то, что делаете все возможное и невозможное — это ценится.