I have a class CarPart
defined as:
class CarPart {
String name;
BigDecimal price;
Supplier supplier;
}
A class Report
:
class Report {
List<Part> parts;
BigDecimal total;
}
and a class Part
:
class Part {
String name;
String supplierName;
}
Given a Stream<CarPart> carParts
, I need to create a Report
object.
My idea to handle this, is to create a Map<List<Part>, BigDecimal>
where the List<Part>
is a list of the transformed CarPart
objects, and the BigDecimal
is the sum of the prices of all car parts in the given stream. Afterwards, I could access the Map<>
which would contain a single entry, and I could create a new Report
.
I started doing that, but I am not sure how to collect it. After the .map
I am doing below, I have in practice a Map<Part, BigDecimal>
but I need to summarize all the Part
objects in a list, and add all the BigDecimal
to create the total value for the Report
.
carParts.stream()
.map(x -> {
return new AbstractMap.SimpleEntry<>(new Part(x.getName(), x.supplier.getName()), x.getPrice());
})
.collect(.....)
Am I handling it completely wrong? I am trying to iterate the stream only once.
P.S: Assume that all getters and setters are available.
If you're on Java 12+:
carParts.collect(teeing(
mapping(p -> new Part(p.name, p.supplier.name), toList()),
mapping(p -> p.price, reducing(BigDecimal.ZERO, BigDecimal::add)),
Report::new
));
Assuming this static import:
import static java.util.stream.Collectors.*;
If a third party library is an option, which offers tuples and tuple collectors (e.g. jOOλ), you can do this even before Java 12
carParts.collect(Tuple.collectors(
mapping(p -> new Part(p.name, p.supplier.name), toList()),
mapping(p -> p.price, reducing(BigDecimal.ZERO, BigDecimal::add))
)).map(Report::new);
You can roll your own Tuple.collectors()
if you want, of course, replacing Tuple2
by Map.Entry
:
static <T, A1, A2, D1, D2> Collector<T, Tuple2<A1, A2>, Tuple2<D1, D2>> collectors(
Collector<? super T, A1, D1> collector1
, Collector<? super T, A2, D2> collector2
) {
return Collector.<T, Tuple2<A1, A2>, Tuple2<D1, D2>>of(
() -> tuple(
collector1.supplier().get()
, collector2.supplier().get()
),
(a, t) -> {
collector1.accumulator().accept(a.v1, t);
collector2.accumulator().accept(a.v2, t);
},
(a1, a2) -> tuple(
collector1.combiner().apply(a1.v1, a2.v1)
, collector2.combiner().apply(a1.v2, a2.v2)
),
a -> tuple(
collector1.finisher().apply(a.v1)
, collector2.finisher().apply(a.v2)
)
);
}
Disclaimer: I made jOOλ
You asked me in the comments to finish what you've started. I don't think that's the right way. The right way is to implement a collector (or use the suggested third party collector, I don't see why that wouldn't be an option) that does the same thing as the JDK 12 collector Collectors.teeing()
.
But here you go, this is one way how you could have completed what you've started:
carParts
// Stream<SimpleEntry<Part, BigDecimal>>
.map(x -> new AbstractMap.SimpleEntry<>(
new Part(x.name, x.supplier.name), x.price))
// Map<Integer, Report>
.collect(Collectors.toMap(
// A dummy map key. I don't really need it, I just want access to
// the Collectors.toMap()'s mergeFunction
e -> 1,
// A single entry report. This just shows that the intermediate
// step of creating a Map.Entry wasn't really useful anyway
e -> new Report(
Collections.singletonList(e.getKey()),
e.getValue()),
// Merging two intermediate reports
(r1, r2) -> {
List<Part> parts = new ArrayList<>();
parts.addAll(r1.parts);
parts.addAll(r2.parts);
return new Report(parts, r1.total.add(r2.total));
}
// We never needed the map.
)).get(1);
There are many other ways to do something similar. You could also use the Stream.collect(Supplier, BiConsumer, BiConsumer)
overload to implement an ad-hoc collector, or use Collector.of()
to create one.
But really. Use some variant of Collectors.teeing()
. Or even an imperative loop, rather than the above.
Report
classWhen the Report
class is mutable and you have the necessary access to modify it, you can use
Report report = carParts.stream()
.collect(
() -> new Report(new ArrayList<>(), BigDecimal.ZERO),
(r, cp) -> {
r.parts.add(new Part(cp.getName(), cp.supplier.getName()));
r.total = r.total.add(cp.getPrice());
},
(r1, r2) -> { r1.parts.addAll(r2.parts); r1.total = r1.total.add(r2.total); });
Report
classWhen you can’t modify Report
instances, you have to use a temporary mutable object for the processing and create a final result object afterwards. Otherwise, the operation is similar:
Report report = carParts.stream()
.collect(Collector.of(
() -> new Object() {
List<Part> parts = new ArrayList<>();
BigDecimal total = BigDecimal.ZERO;
},
(r, cp) -> {
r.parts.add(new Part(cp.getName(), cp.supplier.getName()));
r.total = r.total.add(cp.getPrice());
},
(r1, r2) -> {
r1.parts.addAll(r2.parts);
r1.total = r1.total.add(r2.total);
return r1;
},
tmp -> new Report(tmp.parts, tmp.total)));
Well, in principle, you don’t need mutable objects but could implement the operation as a pure Reduction, however a Mutable Reduction aka collect
operation is more efficient for this specific purpose (i.e. when collecting values into a List
).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With