7

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,450
  • 2
  • 28
  • 46
  • 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

2 Answers2

3

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
  • 110,516
  • 12
  • 173
  • 277
  • 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
0

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,080
  • 2
  • 4
  • 10