Как сгруппировать свойства объекта и сопоставить его с другим объектом, используя потоки Java 8?

Предположим, у меня есть группа бамперных машинок, на боках которых есть размер, цвет и идентификатор (код машины).

class BumperCar {
    int size;
    String color;
    String carCode;
}

Теперь мне нужно сопоставить бамперные машинки с List из DistGroup объектов, каждый из которых содержит свойства size, color и List кодов автомобилей.

class DistGroup {
    int size;
    Color color;
    List<String> carCodes;

    void addCarCodes(List<String> carCodes) {
        this.carCodes.addAll(carCodes);
    }
}

Например,

[
    BumperCar(size=3, color=yellow, carCode=Q4M),
    BumperCar(size=3, color=yellow, carCode=T5A),
    BumperCar(size=3, color=red, carCode=6NR)
]

должно привести к:

[
    DistGroup(size=3, color=yellow, carCodes=[ Q4M, T5A ]),
    DistGroup(size=3, color=red, carCodes=[ 6NR ])
]

Я попробовал следующее, что на самом деле делает то, что я хочу. Но проблема в том, что он материализует промежуточный результат (в Map), и я также думаю, что это можно сделать сразу (возможно, используя mapping или collectingAndThen или reducing или что-то в этом роде), что приведет к более элегантному коду.

List<BumperCar> bumperCars = ...
Map<SizeColorCombination, List<BumperCar>> map = bumperCars.stream()
    .collect(groupingBy(t -> new SizeColorCombination(t.getSize(), t.getColor())));

List<DistGroup> distGroups = map.entrySet().stream()
    .map(t -> {
        DistGroup d = new DistGroup(t.getKey().getSize(), t.getKey().getColor());
        d.addCarCodes(t.getValue().stream()
            .map(BumperCar::getCarCode)
            .collect(toList()));
        return d;
    })
    .collect(toList());

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

Изменить. Как получить желаемый результат, не материализуя промежуточный результат? Я просто ищу способ, который не материализует промежуточный результат, по крайней мере, на поверхности. Это означает, что я предпочитаю не использовать что-то вроде этого:

something.stream()
    .collect(...) // Materializing
    .stream()
    .collect(...); // Materializing second time

Конечно, если это возможно.


Обратите внимание, что для краткости я опустил геттеры и конструкторы. Вы также можете предположить, что методы equals и hashCode реализованы правильно. Также обратите внимание, что я использую SizeColorCombination, который я использую в качестве ключа группировки. Этот класс, очевидно, содержит свойства size и color. Также можно использовать такие классы, как Tuple или Pair, или любой другой класс, представляющий комбинацию двух произвольных значений.
Изменить: также обратите внимание, что вместо этого, конечно, можно использовать старый цикл for. , но это выходит за рамки данного вопроса.


person MC Emperor    schedule 18.01.2019    source источник
comment
Как примечание, groupingBy() по умолчанию группирует значения в List, поэтому toList() можно опустить.   -  person Lino    schedule 18.01.2019
comment
Идея использования потоков состоит в том, чтобы сделать код более читабельным, более понятным (за счет производительности) или массово распараллеливаемым без шаблонного кода. Это не современное универсальное решение для замены старых способов. Код, который вы предоставили, в лучшем случае загадочен. Я предлагаю использовать классический цикл for, который в этом случае намного чище.   -  person Mark Jeronimus    schedule 18.01.2019
comment
@ Лино Ты прав. Я удалил его.   -  person MC Emperor    schedule 18.01.2019
comment
@MarkJeronimus Правильно, вот почему я не удовлетворен своим текущим решением и ищу элегантный способ добиться того же результата - если он существует. В противном случае я с удовольствием вернусь к классической петле.   -  person MC Emperor    schedule 18.01.2019


Ответы (4)


Если мы предположим, что DistGroup имеет hashCode/equals на основе size и color, вы можете сделать это следующим образом:

bumperCars
    .stream()
    .map(x -> {
        List<String> list = new ArrayList<>();
        list.add(x.getCarCode());
        return new SimpleEntry<>(x, list);
    })
    .map(x -> new DistGroup(x.getKey().getSize(), x.getKey().getColor(), x.getValue()))
    .collect(Collectors.toMap(
        Function.identity(),
        Function.identity(),
        (left, right) -> {
            left.getCarCodes().addAll(right.getCarCodes());
            return left;
        }))
    .values(); // Collection<DistGroup>
person Eugene    schedule 18.01.2019
comment
Спасибо, этот код работает для меня. Обратите внимание, что после использования этого кода я немного изменил код, поэтому машинки с бамперами напрямую сопоставлены с DistGroup с помощью .map(t -> new DistGroup(t.getSize(), t.getColor(), new ArrayList<>(Arrays.asList(t.getCarCode())))). Затем я собираю его с помощью toMap с теми же аргументами, что и ваш код, но с первым аргументом t -> new SimpleEntry<>(t.getColor(), t.getSize()) вместо Function.identity(). - person MC Emperor; 21.01.2019

Решение-1

Просто объединив два шага в один:

List<DistGroup> distGroups = bumperCars.stream()
        .collect(Collectors.groupingBy(t -> new SizeColorCombination(t.getSize(), t.getColor())))
        .entrySet().stream()
        .map(t -> {
            DistGroup d = new DistGroup(t.getKey().getSize(), t.getKey().getColor());
            d.addCarCodes(t.getValue().stream().map(BumperCar::getCarCode).collect(Collectors.toList()));
            return d;
        })
        .collect(Collectors.toList());

Решение-2

Ваша промежуточная переменная была бы намного лучше, если бы вы могли использовать groupingBy дважды, используя оба атрибута, и отображать значения как List кодов, например:

Map<Integer, Map<String, List<String>>> sizeGroupedData = bumperCars.stream()
        .collect(Collectors.groupingBy(BumperCar::getSize,
                Collectors.groupingBy(BumperCar::getColor,
                        Collectors.mapping(BumperCar::getCarCode, Collectors.toList()))));

и просто используйте forEach, чтобы добавить в окончательный список как:

List<DistGroup> distGroups = new ArrayList<>();
sizeGroupedData.forEach((size, colorGrouped) ->
        colorGrouped.forEach((color, carCodes) -> distGroups.add(new DistGroup(size, color, carCodes))));

Примечание. Я обновил ваш конструктор, чтобы он принимал список кодов карт.

DistGroup(int size, String color, List<String> carCodes) {
    this.size = size;
    this.color = color;
    addCarCodes(carCodes);
}

Дальнейшее объединение второго решения в одно полное утверждение (хотя, честно говоря, я бы сам предпочел forEach):

List<DistGroup> distGroups = bumperCars.stream()
        .collect(Collectors.groupingBy(BumperCar::getSize,
                Collectors.groupingBy(BumperCar::getColor,
                        Collectors.mapping(BumperCar::getCarCode, Collectors.toList()))))
        .entrySet()
        .stream()
        .flatMap(a -> a.getValue().entrySet()
                .stream().map(b -> new DistGroup(a.getKey(), b.getKey(), b.getValue())))
        .collect(Collectors.toList());
person Naman    schedule 18.01.2019

Вы можете собирать, используя BiConsumer, которые принимают (HashMap<SizeColorCombination, DistGroup> res, BumperCar bc) в качестве параметров

Collection<DistGroup> values = bumperCars.stream()
        .collect(HashMap::new, (HashMap<SizeColorCombination, DistGroup> res, BumperCar bc) -> {
                SizeColorCombination dg = new SizeColorCombination(bc.color, bc.size);
                DistGroup distGroup = res.get(dg);
                if(distGroup != null) {
                    distGroup.addCarCode(bc.carCode);
                }else {
                    List<String> codes = new ArrayList();
                    distGroup = new DistGroup(bc.size, bc.color, codes);
                    res.put(dg, distGroup);
                }
                },HashMap::putAll).values();
person SEY_91    schedule 18.01.2019

Посмотрите мою библиотеку AbacusUtil:

StreamEx.of(bumperCars)
         .groupBy(c -> Tuple.of(c.getSize(), c.getColor()), BumperCar::getCarCode)
         .map(e -> new DistGroup(e.getKey()._1, e.getKey()._2, e.getValue())
         .toList();
person 123-xyz    schedule 19.01.2019