7

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.

Lii
  • 11,553
  • 8
  • 64
  • 88
maria82
  • 73
  • 4

2 Answers2

6

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.

Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509
  • 1
    A simpler Java-8 compatible `teeing` implementation can be found in [this answer.](https://stackoverflow.com/a/54030899/1746118) – Naman May 20 '21 at 17:35
  • Thanks @LukasEder ! Unfortunately, I am running on Java 8.. How could I do this without a 3rd party library? – maria82 May 20 '21 at 17:49
  • @maria82: You copy paste the `collectors()` method and adapt it to your needs, or follow the link by @Naman – Lukas Eder May 20 '21 at 17:49
  • @maria82: No, it's not totally wrong, it just feels more laborious. Having such a teeing utility in your codebase will be useful time and again, irrespective of whether you're using the Java 12 one, a third party one, or your own. – Lukas Eder May 20 '21 at 17:59
  • @LukasEder would you mind providing an example based on my laborious approach? I am just curious to see how I could combine Collectors to achieve what I need. I have tried many many things. I would appreciate an example if you have the time. – maria82 May 20 '21 at 22:59
  • @maria82: Well, I really don't recommend continuing with what you started, but I mean, many roads lead to Rome :) – Lukas Eder May 21 '21 at 06:36
  • @LukasEder Thank you very very much Lukas! I really appreciate it – maria82 May 21 '21 at 08:53
  • 1
    Your `.collect(Collectors.toMap(e -> 1, …)).get(1)` is a `reduce` in disguise. Combine the `.map(x -> new AbstractMap.SimpleEntry<>(new Part(x.name, x.supplier.name), x.price))` and the `e -> new Report(Collections.singletonList(e.getKey()), e.getValue())` function to a single `.map(x -> new Report(Collections.singletonList(new Part(x.name, x.supplier.name)), x.price))`. Then, use the merge function as reduction function, e.g. `.reduce( /* the merge function without any changes */).orElseGet(() -> new Report(Collections.emptyList(), BigDecimal.ZERO))`. – Holger May 25 '21 at 11:48
  • Thanks, @Holger: I would not have added such a silly `collect()` solution to my answer if it hadn't been an explicit request to complete what had already been started. I could have used `Collectors.reducing()` maybe, but is it even worth the trouble? I added a disclaimer, and the best solution here is to use `teeing()` – Lukas Eder May 25 '21 at 12:05
3

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

Holger
  • 285,553
  • 42
  • 434
  • 765