8

I have something like this:

Integer totalIncome = carDealer.getBrands().stream().mapToInt(brand -> brand.getManufacturer().getIncome()).sum();
Integer totalOutcome = carDealer.getBrands().stream().mapToInt(brand -> brand.getManufacturer().getOutcome()).sum();

How could I write that in one stream ? to collect f.e. Pair<Integer, Integer> with totalIncome and totalOutcome ?

EDITED:

Thank you guys for your comments, answers, and involvment. I would have a question about different approach to that problem using streams. What do you think about that:

final IncomeAndOutcome incomeAndOutcome = carDealer.getBrands()
                    .stream()
                    .map(Brand::getManufacturer)
                    .map(IncomeAndOutcome::of)
                    .reduce(IncomeAndOutcome.ZERO, IncomeAndOutcome::sum);

static class IncomeAndOutcome {

    private static final IncomeAndOutcome ZERO = of(0, 0);

    @Getter
    private final int income;

    @Getter
    private final int outcome;

    public static IncomeAndOutcome of(final int income, final int outcome) {
        return new IncomeAndOutcome(income, outcome);
    }

    public static IncomeAndOutcome of(final Manufacturer manufacturer) {
        return new IncomeAndOutcome(manufacturer.getIncome(), manufacturer.getOutcome());
    }

    IncomeAndOutcome(final int income, final int outcome) {
        this.income = income;
        this.outcome = outcome;
    }

    IncomeAndOutcome sum(final IncomeAndOutcome incomeAndOutcome) {
        return of(this.income + incomeAndOutcome.getIncome(), this.outcome + incomeAndOutcome.getOutcome());
    }
}
user3529850
  • 1,632
  • 5
  • 32
  • 51
  • 1
    Did you mean `return new Pair<>(totalIncome, totalOutcome);`? – Bob Dalgleish Nov 16 '17 at 18:46
  • Yes, but I think calling stream two times isn't efficient (first time fo `income` and second time for `outcome`). And I was wondering if I could join them to ultimately get just one stream that return sum of `income` and sum of `outcome` ? – user3529850 Nov 16 '17 at 18:50
  • If you want you iterate once use for loop and increment two sums based on two fields. – Pshemo Nov 16 '17 at 18:51
  • You are right, but I thought that maybe I could use streams for that (instead of a for loop). Can I ? – user3529850 Nov 16 '17 at 18:52
  • 6
    Even if you could, would it be more readable than a simple loop? – azurefrog Nov 16 '17 at 18:55
  • 1
    If I'm not mistaken, you want to map each element to pair of those values (maybe as `new int[2]` arrays) so you would consume more memory (and spend some time) to create those arrays. If you are not using some parallel processing which streams can simplify I suspect that simple loop would be more efficient and easier to read. Otherwise you could probably skip mapping part and directly `reduce(...)` elements to some Pair, but that still would not look nicer. – Pshemo Nov 16 '17 at 18:58
  • Ok. I agree. You are rigth. Thanks. – user3529850 Nov 16 '17 at 19:02
  • @Pshemo it *might* be true - without measuring this is pure guessing – Eugene Nov 16 '17 at 20:31

3 Answers3

2

Without measuring correctly - everything is guessing. The only argument I do agree with is about readability - this is hardly the case here; but in case you wanted to know this for academic purposes, you can do it:

int[] result = carDealer.getBrands()
         .stream()
         .map(brand -> new int[]{brand.getManufacturer().getIncome(),
                                 brand.getManufacturer().getOutcome()})
         .collect(Collector.of(
                    () -> new int[2],
                    (left, right) -> {
                        left[0] += right[0];
                        left[1] += right[1];
                    },
                    (left, right) -> {
                        left[0] += right[0];
                        left[1] += right[1];
                        return left;
                    }));
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • 2
    Reduction should be stateless. You should probably use `collect()` instead, with an identical `accumulator` and `combiner`. – shmosel Nov 16 '17 at 20:53
  • You don't need a `Collector` unless you're passing characteristics or a finisher. Just call [`collect(supplier, accumulator, combiner)`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#collect-java.util.function.Supplier-java.util.function.BiConsumer-java.util.function.BiConsumer-). – shmosel Nov 16 '17 at 22:21
  • Updated my post with different approach, could you give me some feedback ? – user3529850 Nov 17 '17 at 09:31
  • 2
    @user3529850 yes, that's a pretty common pattern to do when you need to handle multiple things at a time; the thing is `reduce` needs to create new instances all the time - as you do it in your code. `collect` does not have this restriction... you can take a look here for example how this can be achieved via a custom `Collector`: https://stackoverflow.com/a/44357446/1059372 – Eugene Nov 17 '17 at 09:36
  • As mentioned here https://stackoverflow.com/questions/42161085/aggregating-more-than-two-properties-java-8 there could be a helper class instead of three lambdas in the collector. – rvazquezglez Dec 04 '20 at 07:02
  • how use this with bigdecimal fields, this could be a approach https://stackoverflow.com/questions/65094934/java-stream-group-by-02-fields-and-aggregate-by-sum-on-2-bigdecimal-fields – qleoz12 Dec 08 '22 at 22:03
2

This will give you total of income & outcome. Here 1st argument of reduce() is the identity. If you are not specifying that reduce() function will give optional value.

Pair<Integer, Integer> result = carDealer.getBrands()
                    .stream()
                    .map(brand -> Pair.of(brand.getManufacturer().getIncome(), brand.getManufacturer().getOutcome()))
                    .reduce(Pair.of(0, 0), (pair1, pair2) -> Pair.of(pair1.getFirst() + pair2.getFirst(), pair1.getSecond() + pair2.getSecond()));
DharmanBot
  • 1,066
  • 2
  • 6
  • 10
Moksh Shah
  • 21
  • 4
  • how i could use this with bigdecimal type – qleoz12 Dec 08 '22 at 22:18
  • 1
    @qleoz12 In reduce function you need to specify operation you need to perform with previous value & next value, assuming Income & Outcome here is in BigDecimal then, `carDealer.getBrands() .stream() .map(brand -> Pair.of(brand.getManufacturer().getIncome(), brand.getManufacturer().getOutcome())) .reduce(Pair.of(BigDecimal.ZERO, BigDecimal.ZERO), (pair1, pair2) -> Pair.of(pair1.getFirst().add(pair2.getFirst()), pair1.getSecond().add(pair2.getSecond())));` – Moksh Shah Dec 12 '22 at 08:30
0

By the way, OpenJDK 12 added this nice Collectors.teeing collector, which allows you to collect using two separate collectors and then combine their results, e.g.:

Pair<Integer, Integer> res =
    carDealer.getBrands().stream()
        .map(Brand::getManufacturer)
        .collect(
            Collectors.teeing(
                Collectors.summingInt(Manufacturer::getIncome),
                Collectors.summingInt(Manufacturer::getOutcome),
                Pair::of));
Oleksandr Pyrohov
  • 14,685
  • 6
  • 61
  • 90