Как сгруппировать свойства объекта и отобразить его на другой объект с помощью Java 8 Streams?
Предположим, у меня есть группа автомобилей-бамперов, у которых есть размер, цвет и идентификатор ("код автомобиля") по бокам.
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, но это не входит в сферу этого вопроса.
Ответы
Ответ 1
Если мы предположим, что 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>
Ответ 2
Решение-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());
Ответ 3
Вы можете собирать данные, используя 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();
Ответ 4
Проверьте мою библиотеку 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();