Как создать карту карт из списка карт с Java 8 streaming api
Фон
У меня есть список карт, которые выглядят примерно так:
[
{
"name": "A",
"old": 0.25,
"new": 0.3
},
{
"name": "B",
"old": 0.3,
"new": 0.35
},
{
"name": "A",
"old": 0.75,
"new": 0.7
},
{
"name": "B",
"old": 0.7,
"new": 0.60
}
]
и я хочу, чтобы результат выглядел так:
{
"A": {
"old": 1,
"new": 1
},
"B": {
"old": 1,
"new": 0.95
}
}
... где значения old
и new
суммируются для каждой связанной записи.
Тип данных списка карт - List<Map<String, Object>>
, поэтому вывод должен быть Map<String, Map<String, Double>>
.
Что я пробовал
С графическим рисунком диаграммы, чтением документации и пробными версиями и ошибками я смог придумать следующее:
data.stream()
.collect(
Collectors.groupingBy(entry -> entry.get("name"),
Collectors.summingDouble(entry ->
Double.parseDouble(entry.get("old").toString())))
);
для создания объекта типа Map<String, Double>
, где выход
{
"A": 1,
"B": 1
}
для суммирования old
значений. Однако я не могу превратить его в карту карт. Что-то вроде этого:
data.stream()
.collect(
Collectors.groupingBy(entry -> entry.get("name"),
Collectors.mapping(
Collectors.groupingBy(entry -> entry.get("old"),
Collectors.summingDouble(entry ->
Double.parseDouble(entry.get("old").toString())
)
),
Collectors.groupingBy(entry -> entry.get("new"),
Collectors.summingDouble(entry ->
Double.parseDouble(entry.get("new").toString())
)
)
)
)
);
не работает, потому что Collectors.mapping()
принимает только одну функцию отображения и нисходящий коллекционер, но я не уверен, как отобразить сразу два значения.
Есть ли еще одна функция, необходимая для создания сопоставлений двух разных значений? Любые предложения относительно лучших способов сделать это также очень ценятся.
Ответы
Ответ 1
Вы можете использовать потоки, но вы также можете использовать методы Map
computeIfAbsent
и merge
:
Map<String, Map<String, Double>> result = new LinkedHashMap<>();
data.forEach(entry -> {
String name = (String) entry.get("name");
Map<String, Double> map = result.computeIfAbsent(name, k -> new HashMap<>());
map.merge("old", (Double) entry.get("old"), Double::sum);
map.merge("new", (Double) entry.get("new"), Double::sum);
});
Ответ 2
Это можно достичь только с помощью инструментов Stream
(аналогично этому):
Map<String, Map<String, Double>> collect = data.stream().collect(
Collectors.groupingBy(m -> (String)m.get("name"),
Collector.of(LinkedHashMap::new,
(acc, e) -> Stream.of("old", "new").forEach(key -> acc.merge(key, (Double) e.get(key), Double::sum)),
(m1, m2) -> {
m2.forEach((k, v) -> m1.merge(k, v, Double::sum));
return m1;
})
));
Существует также способ Java 8:
Map<String, Map<String, Double>> stats = data.stream().collect(
Collectors.groupingBy(m -> (String) m.get("name"),
Collectors.flatMapping(m -> m.entrySet().stream().filter(e -> !"name".equals(e.getKey())),
Collectors.toMap(Map.Entry::getKey, e -> (Double)e.getValue(), Double::sum, LinkedHashMap::new)
)
));
Ответ 3
Вы приблизились к решению с первой попытки, однако вам нужно сделать какой-то пользовательский код, чтобы полностью завершить изображение.
Вам понадобится реализовать свой собственный коллекционер, который превратит несколько карт в одну двойную карту.
Это выглядит так:
Collector.of(
() -> new HashMap<>(),
(Map<String, Double>target, Map<String, Object> source) -> {
target.merge("old", (Double)source.get("old"), Double::sum);
target.merge("new", (Double)source.get("new"), Double::sum);
},
(Map<String, Double> map1, Map<String, Double> map2) -> {
map2.forEach((k, v) -> map1.merge(k, v, Double::sum));
return map1;
}
)
Это в сочетании с вашей первоначальной группировкой по попытке решает картину:
data.stream()
.collect(
Collectors.groupingBy(entry -> entry.get("name"),
// Insert collector here
)
);
Пример полного кода онлайн: http://tpcg.io/pJftrJ
Ответ 4
Вы можете объявить класс под названием " Pair
public class Pair {
private final double oldVal;
private final double newVal;
public Pair(double oldVal, double newVal) {
super();
this.oldVal = oldVal;
this.newVal = newVal;
}
public double getOldVal() {
return oldVal;
}
public double getNewVal() {
return newVal;
}
@Override
public String toString() {
return "{oldVal=" + oldVal + ", newVal=" + newVal + "}";
}
}
Тогда сделайте это так,
Map<Object, Pair> result = sourceMaps.stream()
.collect(Collectors.toMap(m -> m.get("name"),
m -> new Pair((double) m.get("old"), (double) m.get("new")),
(p1, p2) -> new Pair(p1.getOldVal() + p2.getOldVal(), p1.getNewVal() + p2.getNewVal())));
И здесь выход,
{A={oldVal=1.0, newVal=1.0}, B={oldVal=1.0, newVal=0.95}}