Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping a stream to an instance of another object

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.

like image 776
maria82 Avatar asked May 20 '21 16:05

maria82


2 Answers

Java 12+ solution

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.*;

Solution using third party 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λ

Solution using Java 8 only and continuing with your attempts

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.

like image 192
Lukas Eder Avatar answered Nov 03 '22 02:11

Lukas Eder


With a mutable Report class

When 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); });

With an immutable Report class

When 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).

like image 30
Holger Avatar answered Nov 03 '22 02:11

Holger