Как отфильтровать данные от вызова метода в коллекторе перед добавлением на карту - PullRequest
0 голосов
/ 03 апреля 2019

У меня есть структура

interface Foo{
   List<Bar> getBars();
}

interface Bar{
   List<Number> getValues();
}

и список List<Foo> foos

Я хочу создать карту, которая будет выглядеть примерно так, за исключением записей, в которых у foo нет баров или у баров нет значений.

{
  fooid :{
              barId : bar
           }
}

очевидно, что моей первой мыслью было сделать

    foos.stream().filter(f->!f.getBars().isEmpty())
    .collect( Collectors.toMap(
        f->f.id(),
        f->f.getBars().stream().filter(b->!b.getValues().isEmpty())
                 .collect(Collectors.toMap( b->b.id(),b->b.getValues()                       
    ))));

проблема в том, что я вызываю метод getValues дважды, и это дорогой метод, что будет лучшей практикой для обработки таких вызовов?

Ответы [ 2 ]

3 голосов
/ 03 апреля 2019

Я думаю это то, что вы ищете:

private static void doMap(List<Foo> foos) {
    foos.stream()
        .filter(foo -> !foo.getBars()
                           .isEmpty())
        .map(foo -> new SimpleEntry<>(foo.getId(), foo.getBars()
                                                      .stream()
                                                      .map(bar -> new SimpleEntry<>(bar.getId(), bar.getValues()))
                                                      .filter(entry -> !entry.getValue()
                                                                             .isEmpty())
                                                      .collect(entriesToMapCollector())))
        .filter(entry -> !entry.getValue().isEmpty())
        .collect(entriesToMapCollector());
}

private static <K, V> Collector<Entry<K, V>, ?, Map<K, V>> entriesToMapCollector() {
    return Collectors.toMap(Entry::getKey, Entry::getValue);
}

(я бы решительно выступил бы за более императивный подход к этому радиудобочитаемость, удобство обслуживания и все возможности - не делайте этого в рабочем коде)

1 голос
/ 04 апреля 2019

Как только вы поймете, что то, что вы делаете со списком Foo и списком Bar, является одной и той же операцией, вы можете решить эту проблему более легко. Эта операция является groupingBy , где id служит ключом, а List служит значениями. Вы добавляете условие, что отображение не должно создаваться, если значения являются пустым списком.

Это был бы простой случай группировки Bar s:

bars.stream()
    .filter(b -> !b.getValues().isEmpty())
    .collect(toMap(b -> b.id(), b -> b.getValues()));

и Foo s:

foos.stream()
    .filter(f -> !f.getBars().isEmpty())
    .collect(toMap(f -> f.id(), f -> groupBars(f.getBars())));

, где groupBars выполняет первый блок кода. Проблема с группировкой bars состоит в том, что вы дважды вызываете getValues, что, как вы упоминаете, стоит дорого. Проблема с группировкой foos состоит в том, что вы фильтруете, прежде чем узнаете, будет ли сопоставление с пустым значением (результат groupBars).

Как показано в Не в ответе JD , решение обеих проблем - кэширование / сохранение. Для Bar s вы хотите кэшировать результат getValues. Для Foo s вы хотите кэшировать результат groupBars, чтобы вы могли фильтровать его. Способ кеширования в потоке с помощью объекта контейнера; в этом случае Map.Entry работает хорошо, но вы можете использовать любой объект хранения данных. Затем вы отображаете свои данные в свой объект (получая их поток), и вы можете получить их данные в разных частях потока.

Следующий метод группирует список Bar s по их id:

static Map<Integer, List<Number>> groupByIdBar(List<Bar> bars) {
       return bars.stream()
                  .map(b -> Map.entry(b.id(), b.getValues()))
                  .filter(e -> !e.getValue().isEmpty())
                  .collect(toMap(Entry::getKey, Entry::getValue));
}

где e.getValue() - это поисковый вызов из нашего «кэша».

Аналогично, этот метод сгруппирует Foo s по id:

static Map<Integer, Map<Integer, List<Number>>> groupByIdFoo(List<Foo> foos) {
       return foos.stream()
                  .map(f -> Map.entry(f.id(), groupByIdBar(f.getBars())))
                  .filter(e -> !e.getValue().isEmpty())
                  .collect(toMap(Entry::getKey, Entry::getValue));
}

, где e.getValue() - результат groupByIdBar, который позволяет постобработку (фильтрацию).

Вы заметите, что методы очень похожи. Это потому, что, как было указано в начале, мы выполняем ту же операцию над ними.

Результат, который вы хотите получить, получается groupByIdFoo(foos).


Зацикливание может быть проще в этом случае:

Map<Integer, Map<Integer, List<Number>>> mapping = new HashMap<>();
for (Foo f : foos) {
    List<Bar> bars = f.getBars();
    if (!bars.isEmpty()) {
        Map<Integer, List<Number>> bMap = new HashMap<>();
        for (Bar b : bars) {
            if (!b.getValues().isEmpty()) {
                bMap.put(b.id(), b.getValues());
            }
        }
        if (!bMap.isEmpty()) {
            mapping .put(f.id(), bMap);
        }
    }
}

Если вы решите пойти по этому пути, вы можете оптимизировать код. Например, вы можете создать экземпляр bMap при первом столкновении с непустым b.getValues().


Для набора данных

Bar b1 = new Bar(1);
b1.values = List.of(10, 20, 30, 40, 50);

Bar b2 = new Bar(2);
b2.values = List.of();

Bar b3 = new Bar(3);
b3.values = List.of(60, 70);

Foo f1 = new Foo(11);
f1.bars = List.of(b1, b2, b3);

Foo f2 = new Foo(22);
f2.bars = List.of();

Foo f3 = new Foo(33);
f3.bars = List.of(b2);

Foo f4 = new Foo(44);
f4.bars = List.of(b1);

List<Foo> foos = List.of(f1, f2, f3, f4);

Оба дают результат:

{11={1=[10, 20, 30, 40, 50], 3=[60, 70]}, 44={1=[10, 20, 30, 40, 50]}}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...