Java stream. Sum two fields in a stream of objects
Asked Answered
C

3

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());
    }
}
Carbonic answered 16/11, 2017 at 18:43 Comment(8)
Did you mean return new Pair<>(totalIncome, totalOutcome);?Potentiality
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 ?Carbonic
If you want you iterate once use for loop and increment two sums based on two fields.Versify
You are right, but I thought that maybe I could use streams for that (instead of a for loop). Can I ?Carbonic
Even if you could, would it be more readable than a simple loop?Marthamarthe
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.Versify
Ok. I agree. You are rigth. Thanks.Carbonic
@Versify it might be true - without measuring this is pure guessingThemistocles
T
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;
                    }));
Themistocles answered 16/11, 2017 at 20:31 Comment(6)
Reduction should be stateless. You should probably use collect() instead, with an identical accumulator and combiner.Naiad
You don't need a Collector unless you're passing characteristics or a finisher. Just call collect(supplier, accumulator, combiner).Naiad
Updated my post with different approach, could you give me some feedback ?Carbonic
@Carbonic 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://mcmap.net/q/1420394/-how-do-i-sort-a-list-of-treesets-with-java8-streamsThemistocles
As mentioned here #42161585 there could be a helper class instead of three lambdas in the collector.Sardine
how use this with bigdecimal fields, this could be a approach #65095434Corrode
D
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()));
Diatomic answered 28/4, 2022 at 5:33 Comment(2)
how i could use this with bigdecimal typeCorrode
@Corrode 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())));Diatomic
U
2

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));
Undertint answered 19/8, 2023 at 9:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.