Java stream group by and sum multiple fields

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP











up vote
7
down vote

favorite
2












I have a List fooList



class Foo 
private String category;
private int amount;
private int price;

... constructor, getters & setters



I would like to group by category and then sum amount aswell as price.



The result will be stored in a map:



Map<Foo, List<Foo>> map = new HashMap<>();


The key is the Foo holding the summarized amount and price, with a list as value for all the objects with the same category.



So far I've tried the following:



Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));


Now I only need to replace the String key with a Foo object holding the summarized amount and price. Here is where I'm stuck. I can't seem to find any way of doing this.







share|improve this question






















  • have you looked at Collectors.summarizingLong?
    – dehasi
    Aug 28 at 12:49










  • Yes but how do you summarize on multiple fields?
    – MatMat
    Aug 28 at 12:52






  • 1




    well only recently I've answered something very close to this... stackoverflow.com/a/52041895/1059372 in jdk-12 seems like there will be a BiCollector that will make your life easier
    – Eugene
    Aug 28 at 12:58







  • 3




    Do I understand you correctly, that the KEY shall hold the sums? And that the List<Foo> shall hold all original instances?
    – jokster
    Aug 28 at 12:59










  • @jokster yes, that is correct!
    – MatMat
    Aug 28 at 13:01














up vote
7
down vote

favorite
2












I have a List fooList



class Foo 
private String category;
private int amount;
private int price;

... constructor, getters & setters



I would like to group by category and then sum amount aswell as price.



The result will be stored in a map:



Map<Foo, List<Foo>> map = new HashMap<>();


The key is the Foo holding the summarized amount and price, with a list as value for all the objects with the same category.



So far I've tried the following:



Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));


Now I only need to replace the String key with a Foo object holding the summarized amount and price. Here is where I'm stuck. I can't seem to find any way of doing this.







share|improve this question






















  • have you looked at Collectors.summarizingLong?
    – dehasi
    Aug 28 at 12:49










  • Yes but how do you summarize on multiple fields?
    – MatMat
    Aug 28 at 12:52






  • 1




    well only recently I've answered something very close to this... stackoverflow.com/a/52041895/1059372 in jdk-12 seems like there will be a BiCollector that will make your life easier
    – Eugene
    Aug 28 at 12:58







  • 3




    Do I understand you correctly, that the KEY shall hold the sums? And that the List<Foo> shall hold all original instances?
    – jokster
    Aug 28 at 12:59










  • @jokster yes, that is correct!
    – MatMat
    Aug 28 at 13:01












up vote
7
down vote

favorite
2









up vote
7
down vote

favorite
2






2





I have a List fooList



class Foo 
private String category;
private int amount;
private int price;

... constructor, getters & setters



I would like to group by category and then sum amount aswell as price.



The result will be stored in a map:



Map<Foo, List<Foo>> map = new HashMap<>();


The key is the Foo holding the summarized amount and price, with a list as value for all the objects with the same category.



So far I've tried the following:



Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));


Now I only need to replace the String key with a Foo object holding the summarized amount and price. Here is where I'm stuck. I can't seem to find any way of doing this.







share|improve this question














I have a List fooList



class Foo 
private String category;
private int amount;
private int price;

... constructor, getters & setters



I would like to group by category and then sum amount aswell as price.



The result will be stored in a map:



Map<Foo, List<Foo>> map = new HashMap<>();


The key is the Foo holding the summarized amount and price, with a list as value for all the objects with the same category.



So far I've tried the following:



Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getCategory()));


Now I only need to replace the String key with a Foo object holding the summarized amount and price. Here is where I'm stuck. I can't seem to find any way of doing this.









share|improve this question













share|improve this question




share|improve this question








edited Aug 28 at 12:52









YCF_L

31.5k103273




31.5k103273










asked Aug 28 at 12:44









MatMat

299214




299214











  • have you looked at Collectors.summarizingLong?
    – dehasi
    Aug 28 at 12:49










  • Yes but how do you summarize on multiple fields?
    – MatMat
    Aug 28 at 12:52






  • 1




    well only recently I've answered something very close to this... stackoverflow.com/a/52041895/1059372 in jdk-12 seems like there will be a BiCollector that will make your life easier
    – Eugene
    Aug 28 at 12:58







  • 3




    Do I understand you correctly, that the KEY shall hold the sums? And that the List<Foo> shall hold all original instances?
    – jokster
    Aug 28 at 12:59










  • @jokster yes, that is correct!
    – MatMat
    Aug 28 at 13:01
















  • have you looked at Collectors.summarizingLong?
    – dehasi
    Aug 28 at 12:49










  • Yes but how do you summarize on multiple fields?
    – MatMat
    Aug 28 at 12:52






  • 1




    well only recently I've answered something very close to this... stackoverflow.com/a/52041895/1059372 in jdk-12 seems like there will be a BiCollector that will make your life easier
    – Eugene
    Aug 28 at 12:58







  • 3




    Do I understand you correctly, that the KEY shall hold the sums? And that the List<Foo> shall hold all original instances?
    – jokster
    Aug 28 at 12:59










  • @jokster yes, that is correct!
    – MatMat
    Aug 28 at 13:01















have you looked at Collectors.summarizingLong?
– dehasi
Aug 28 at 12:49




have you looked at Collectors.summarizingLong?
– dehasi
Aug 28 at 12:49












Yes but how do you summarize on multiple fields?
– MatMat
Aug 28 at 12:52




Yes but how do you summarize on multiple fields?
– MatMat
Aug 28 at 12:52




1




1




well only recently I've answered something very close to this... stackoverflow.com/a/52041895/1059372 in jdk-12 seems like there will be a BiCollector that will make your life easier
– Eugene
Aug 28 at 12:58





well only recently I've answered something very close to this... stackoverflow.com/a/52041895/1059372 in jdk-12 seems like there will be a BiCollector that will make your life easier
– Eugene
Aug 28 at 12:58





3




3




Do I understand you correctly, that the KEY shall hold the sums? And that the List<Foo> shall hold all original instances?
– jokster
Aug 28 at 12:59




Do I understand you correctly, that the KEY shall hold the sums? And that the List<Foo> shall hold all original instances?
– jokster
Aug 28 at 12:59












@jokster yes, that is correct!
– MatMat
Aug 28 at 13:01




@jokster yes, that is correct!
– MatMat
Aug 28 at 13:01












5 Answers
5






active

oldest

votes

















up vote
9
down vote



accepted










A bit ugly, but it should work:



list.stream().collect(Collectors.groupingBy(Foo::getCategory))
.entrySet().stream()
.collect(Collectors.toMap(x ->
int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
return new Foo(x.getKey(), sumAmount, sumPrice);
, Map.Entry::getValue));





share|improve this answer



























    up vote
    4
    down vote













    My variation of Sweepers answer uses a reducing Collector instead of streaming twice to sum the individual fields:



     Map<Foo, List<Foo>> map = fooList.stream()
    .collect(Collectors.groupingBy(Foo::getCategory))
    .entrySet().stream()
    .collect(Collectors.toMap(e -> e.getValue().stream().collect(
    Collectors.reducing((l, r) -> new Foo(l.getCategory(),
    l.getAmount() + r.getAmount(),
    l.getPrice() + r.getPrice())))
    .get(),
    e -> e.getValue()));


    It is not really better though, as it creates a lot of short-lived Foos.



    Note however that Foo is required to provide hashCode- and equals-implementations that take only category into account for the resulting map to work correctly. This would probably not be what you want for Foos in general. I would prefer defining a separate FooSummary class to contain the aggregated data.






    share|improve this answer





























      up vote
      3
      down vote













      If you have a special, dedicated constructor and hashCode and equals methods consistently implemented in Foo as follows:



      public Foo(Foo that) // not a copy constructor!!!
      this.category = that.category;
      this.amount = 0;
      this.price = 0;


      public int hashCode()
      return Objects.hashCode(category);


      public boolean equals(Object another)
      if (another == this) return true;
      if (!(another instanceof Foo)) return false;
      Foo that = (Foo) another;
      return Objects.equals(this.category, that.category);



      The hashCode and equals implementations above allow you to use Foo as a meaningful key in the map (otherwise your map would be broken).



      Now, with the help of a new method in Foo that performs the aggregation of the amount and price attributes at the same time, you can do what you want in 2 steps. First the method:



      public void aggregate(Foo that) 
      this.amount += that.amount;
      this.price += that.price;



      Now the final solution:



      Map<Foo, List<Foo>> result = fooList.stream().collect(
      Collectors.collectingAndThen(
      Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
      m -> m.forEach((k, v) -> v.forEach(k::aggregate)); return m; ));



      EDIT: a few observations were missing...



      On one hand, this solution forces you to use an implementation of hashCode and equals that considers two different Foo instances as equal if they belong to the same category. Maybe this is not desired, or you already have an implementation that takes more or other attributes into account.



      On the other hand, using Foo as the key of a map which is used to group instances by one of its attributes is quite an uncommon use case. I think it would be better to just use the category attribute to group by category and have two maps: Map<String, List<Foo>> to keep the groups and Map<String, Foo> to keep the aggregated price and amount, with the key being the category in both cases.



      Besides this, this solution mutates the keys of the map after the entries are put into it. This is dangerous, because this could break the map. However, here I'm only mutating attributes of Foo that don't participate in neither hashCode nor equals Foo's implementation. I think that this risk is acceptable in this case, due to the unusuality of the requirement.






      share|improve this answer





























        up vote
        0
        down vote













        I suggest you create a helper class, which will hold amount and price



        final class Pair 
        final int amount;
        final int price;

        Pair(int amount, int price)
        this.amount = amount;
        this.price = price;




        And then just collect list to map:



        List<Foo> list =//....;

        Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
        foo -> new Pair(foo.getAmount(), foo.getPrice()),
        (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));





        share|improve this answer




















        • that is a good start, but not what the OP needs... read the comments under the question
          – Eugene
          Aug 28 at 13:06

















        up vote
        0
        down vote













        My take on a solution :)



        public static void main(String args) 
        List<Foo> foos = new ArrayList<>();
        foos.add(new Foo("A", 1, 10));
        foos.add(new Foo("A", 2, 10));
        foos.add(new Foo("A", 3, 10));
        foos.add(new Foo("B", 1, 10));
        foos.add(new Foo("C", 1, 10));
        foos.add(new Foo("C", 5, 10));

        List<Foo> summarized = new ArrayList<>();
        Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>()
        @Override
        public Foo apply(Foo t)
        Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
        Foo f;
        if (!fOpt.isPresent())
        f = new Foo(t.getCategory(), 0, 0);
        summarized.add(f);
        else
        f = fOpt.get();

        f.setAmount(f.getAmount() + t.getAmount());
        f.setPrice(f.getPrice() + t.getPrice());
        return f;

        ));
        System.out.println(collect);






        share|improve this answer






















          Your Answer





          StackExchange.ifUsing("editor", function ()
          StackExchange.using("externalEditor", function ()
          StackExchange.using("snippets", function ()
          StackExchange.snippets.init();
          );
          );
          , "code-snippets");

          StackExchange.ready(function()
          var channelOptions =
          tags: "".split(" "),
          id: "1"
          ;
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function()
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled)
          StackExchange.using("snippets", function()
          createEditor();
          );

          else
          createEditor();

          );

          function createEditor()
          StackExchange.prepareEditor(
          heartbeatType: 'answer',
          convertImagesToLinks: true,
          noModals: false,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: 10,
          bindNavPrevention: true,
          postfix: "",
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          );



          );













           

          draft saved


          draft discarded


















          StackExchange.ready(
          function ()
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f52058261%2fjava-stream-group-by-and-sum-multiple-fields%23new-answer', 'question_page');

          );

          Post as a guest






























          5 Answers
          5






          active

          oldest

          votes








          5 Answers
          5






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          up vote
          9
          down vote



          accepted










          A bit ugly, but it should work:



          list.stream().collect(Collectors.groupingBy(Foo::getCategory))
          .entrySet().stream()
          .collect(Collectors.toMap(x ->
          int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
          int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
          return new Foo(x.getKey(), sumAmount, sumPrice);
          , Map.Entry::getValue));





          share|improve this answer
























            up vote
            9
            down vote



            accepted










            A bit ugly, but it should work:



            list.stream().collect(Collectors.groupingBy(Foo::getCategory))
            .entrySet().stream()
            .collect(Collectors.toMap(x ->
            int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
            int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
            return new Foo(x.getKey(), sumAmount, sumPrice);
            , Map.Entry::getValue));





            share|improve this answer






















              up vote
              9
              down vote



              accepted







              up vote
              9
              down vote



              accepted






              A bit ugly, but it should work:



              list.stream().collect(Collectors.groupingBy(Foo::getCategory))
              .entrySet().stream()
              .collect(Collectors.toMap(x ->
              int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
              int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
              return new Foo(x.getKey(), sumAmount, sumPrice);
              , Map.Entry::getValue));





              share|improve this answer












              A bit ugly, but it should work:



              list.stream().collect(Collectors.groupingBy(Foo::getCategory))
              .entrySet().stream()
              .collect(Collectors.toMap(x ->
              int sumAmount = x.getValue().stream().mapToInt(Foo::getAmount).sum();
              int sumPrice= x.getValue().stream().mapToInt(Foo::getPrice).sum();
              return new Foo(x.getKey(), sumAmount, sumPrice);
              , Map.Entry::getValue));






              share|improve this answer












              share|improve this answer



              share|improve this answer










              answered Aug 28 at 13:21









              Sweeper

              54.7k960117




              54.7k960117






















                  up vote
                  4
                  down vote













                  My variation of Sweepers answer uses a reducing Collector instead of streaming twice to sum the individual fields:



                   Map<Foo, List<Foo>> map = fooList.stream()
                  .collect(Collectors.groupingBy(Foo::getCategory))
                  .entrySet().stream()
                  .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                  Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                  l.getAmount() + r.getAmount(),
                  l.getPrice() + r.getPrice())))
                  .get(),
                  e -> e.getValue()));


                  It is not really better though, as it creates a lot of short-lived Foos.



                  Note however that Foo is required to provide hashCode- and equals-implementations that take only category into account for the resulting map to work correctly. This would probably not be what you want for Foos in general. I would prefer defining a separate FooSummary class to contain the aggregated data.






                  share|improve this answer


























                    up vote
                    4
                    down vote













                    My variation of Sweepers answer uses a reducing Collector instead of streaming twice to sum the individual fields:



                     Map<Foo, List<Foo>> map = fooList.stream()
                    .collect(Collectors.groupingBy(Foo::getCategory))
                    .entrySet().stream()
                    .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                    Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                    l.getAmount() + r.getAmount(),
                    l.getPrice() + r.getPrice())))
                    .get(),
                    e -> e.getValue()));


                    It is not really better though, as it creates a lot of short-lived Foos.



                    Note however that Foo is required to provide hashCode- and equals-implementations that take only category into account for the resulting map to work correctly. This would probably not be what you want for Foos in general. I would prefer defining a separate FooSummary class to contain the aggregated data.






                    share|improve this answer
























                      up vote
                      4
                      down vote










                      up vote
                      4
                      down vote









                      My variation of Sweepers answer uses a reducing Collector instead of streaming twice to sum the individual fields:



                       Map<Foo, List<Foo>> map = fooList.stream()
                      .collect(Collectors.groupingBy(Foo::getCategory))
                      .entrySet().stream()
                      .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                      Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                      l.getAmount() + r.getAmount(),
                      l.getPrice() + r.getPrice())))
                      .get(),
                      e -> e.getValue()));


                      It is not really better though, as it creates a lot of short-lived Foos.



                      Note however that Foo is required to provide hashCode- and equals-implementations that take only category into account for the resulting map to work correctly. This would probably not be what you want for Foos in general. I would prefer defining a separate FooSummary class to contain the aggregated data.






                      share|improve this answer














                      My variation of Sweepers answer uses a reducing Collector instead of streaming twice to sum the individual fields:



                       Map<Foo, List<Foo>> map = fooList.stream()
                      .collect(Collectors.groupingBy(Foo::getCategory))
                      .entrySet().stream()
                      .collect(Collectors.toMap(e -> e.getValue().stream().collect(
                      Collectors.reducing((l, r) -> new Foo(l.getCategory(),
                      l.getAmount() + r.getAmount(),
                      l.getPrice() + r.getPrice())))
                      .get(),
                      e -> e.getValue()));


                      It is not really better though, as it creates a lot of short-lived Foos.



                      Note however that Foo is required to provide hashCode- and equals-implementations that take only category into account for the resulting map to work correctly. This would probably not be what you want for Foos in general. I would prefer defining a separate FooSummary class to contain the aggregated data.







                      share|improve this answer














                      share|improve this answer



                      share|improve this answer








                      edited Aug 29 at 6:37

























                      answered Aug 28 at 13:45









                      Hulk

                      2,13411431




                      2,13411431




















                          up vote
                          3
                          down vote













                          If you have a special, dedicated constructor and hashCode and equals methods consistently implemented in Foo as follows:



                          public Foo(Foo that) // not a copy constructor!!!
                          this.category = that.category;
                          this.amount = 0;
                          this.price = 0;


                          public int hashCode()
                          return Objects.hashCode(category);


                          public boolean equals(Object another)
                          if (another == this) return true;
                          if (!(another instanceof Foo)) return false;
                          Foo that = (Foo) another;
                          return Objects.equals(this.category, that.category);



                          The hashCode and equals implementations above allow you to use Foo as a meaningful key in the map (otherwise your map would be broken).



                          Now, with the help of a new method in Foo that performs the aggregation of the amount and price attributes at the same time, you can do what you want in 2 steps. First the method:



                          public void aggregate(Foo that) 
                          this.amount += that.amount;
                          this.price += that.price;



                          Now the final solution:



                          Map<Foo, List<Foo>> result = fooList.stream().collect(
                          Collectors.collectingAndThen(
                          Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
                          m -> m.forEach((k, v) -> v.forEach(k::aggregate)); return m; ));



                          EDIT: a few observations were missing...



                          On one hand, this solution forces you to use an implementation of hashCode and equals that considers two different Foo instances as equal if they belong to the same category. Maybe this is not desired, or you already have an implementation that takes more or other attributes into account.



                          On the other hand, using Foo as the key of a map which is used to group instances by one of its attributes is quite an uncommon use case. I think it would be better to just use the category attribute to group by category and have two maps: Map<String, List<Foo>> to keep the groups and Map<String, Foo> to keep the aggregated price and amount, with the key being the category in both cases.



                          Besides this, this solution mutates the keys of the map after the entries are put into it. This is dangerous, because this could break the map. However, here I'm only mutating attributes of Foo that don't participate in neither hashCode nor equals Foo's implementation. I think that this risk is acceptable in this case, due to the unusuality of the requirement.






                          share|improve this answer


























                            up vote
                            3
                            down vote













                            If you have a special, dedicated constructor and hashCode and equals methods consistently implemented in Foo as follows:



                            public Foo(Foo that) // not a copy constructor!!!
                            this.category = that.category;
                            this.amount = 0;
                            this.price = 0;


                            public int hashCode()
                            return Objects.hashCode(category);


                            public boolean equals(Object another)
                            if (another == this) return true;
                            if (!(another instanceof Foo)) return false;
                            Foo that = (Foo) another;
                            return Objects.equals(this.category, that.category);



                            The hashCode and equals implementations above allow you to use Foo as a meaningful key in the map (otherwise your map would be broken).



                            Now, with the help of a new method in Foo that performs the aggregation of the amount and price attributes at the same time, you can do what you want in 2 steps. First the method:



                            public void aggregate(Foo that) 
                            this.amount += that.amount;
                            this.price += that.price;



                            Now the final solution:



                            Map<Foo, List<Foo>> result = fooList.stream().collect(
                            Collectors.collectingAndThen(
                            Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
                            m -> m.forEach((k, v) -> v.forEach(k::aggregate)); return m; ));



                            EDIT: a few observations were missing...



                            On one hand, this solution forces you to use an implementation of hashCode and equals that considers two different Foo instances as equal if they belong to the same category. Maybe this is not desired, or you already have an implementation that takes more or other attributes into account.



                            On the other hand, using Foo as the key of a map which is used to group instances by one of its attributes is quite an uncommon use case. I think it would be better to just use the category attribute to group by category and have two maps: Map<String, List<Foo>> to keep the groups and Map<String, Foo> to keep the aggregated price and amount, with the key being the category in both cases.



                            Besides this, this solution mutates the keys of the map after the entries are put into it. This is dangerous, because this could break the map. However, here I'm only mutating attributes of Foo that don't participate in neither hashCode nor equals Foo's implementation. I think that this risk is acceptable in this case, due to the unusuality of the requirement.






                            share|improve this answer
























                              up vote
                              3
                              down vote










                              up vote
                              3
                              down vote









                              If you have a special, dedicated constructor and hashCode and equals methods consistently implemented in Foo as follows:



                              public Foo(Foo that) // not a copy constructor!!!
                              this.category = that.category;
                              this.amount = 0;
                              this.price = 0;


                              public int hashCode()
                              return Objects.hashCode(category);


                              public boolean equals(Object another)
                              if (another == this) return true;
                              if (!(another instanceof Foo)) return false;
                              Foo that = (Foo) another;
                              return Objects.equals(this.category, that.category);



                              The hashCode and equals implementations above allow you to use Foo as a meaningful key in the map (otherwise your map would be broken).



                              Now, with the help of a new method in Foo that performs the aggregation of the amount and price attributes at the same time, you can do what you want in 2 steps. First the method:



                              public void aggregate(Foo that) 
                              this.amount += that.amount;
                              this.price += that.price;



                              Now the final solution:



                              Map<Foo, List<Foo>> result = fooList.stream().collect(
                              Collectors.collectingAndThen(
                              Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
                              m -> m.forEach((k, v) -> v.forEach(k::aggregate)); return m; ));



                              EDIT: a few observations were missing...



                              On one hand, this solution forces you to use an implementation of hashCode and equals that considers two different Foo instances as equal if they belong to the same category. Maybe this is not desired, or you already have an implementation that takes more or other attributes into account.



                              On the other hand, using Foo as the key of a map which is used to group instances by one of its attributes is quite an uncommon use case. I think it would be better to just use the category attribute to group by category and have two maps: Map<String, List<Foo>> to keep the groups and Map<String, Foo> to keep the aggregated price and amount, with the key being the category in both cases.



                              Besides this, this solution mutates the keys of the map after the entries are put into it. This is dangerous, because this could break the map. However, here I'm only mutating attributes of Foo that don't participate in neither hashCode nor equals Foo's implementation. I think that this risk is acceptable in this case, due to the unusuality of the requirement.






                              share|improve this answer














                              If you have a special, dedicated constructor and hashCode and equals methods consistently implemented in Foo as follows:



                              public Foo(Foo that) // not a copy constructor!!!
                              this.category = that.category;
                              this.amount = 0;
                              this.price = 0;


                              public int hashCode()
                              return Objects.hashCode(category);


                              public boolean equals(Object another)
                              if (another == this) return true;
                              if (!(another instanceof Foo)) return false;
                              Foo that = (Foo) another;
                              return Objects.equals(this.category, that.category);



                              The hashCode and equals implementations above allow you to use Foo as a meaningful key in the map (otherwise your map would be broken).



                              Now, with the help of a new method in Foo that performs the aggregation of the amount and price attributes at the same time, you can do what you want in 2 steps. First the method:



                              public void aggregate(Foo that) 
                              this.amount += that.amount;
                              this.price += that.price;



                              Now the final solution:



                              Map<Foo, List<Foo>> result = fooList.stream().collect(
                              Collectors.collectingAndThen(
                              Collectors.groupingBy(Foo::new), // works: special ctor, hashCode & equals
                              m -> m.forEach((k, v) -> v.forEach(k::aggregate)); return m; ));



                              EDIT: a few observations were missing...



                              On one hand, this solution forces you to use an implementation of hashCode and equals that considers two different Foo instances as equal if they belong to the same category. Maybe this is not desired, or you already have an implementation that takes more or other attributes into account.



                              On the other hand, using Foo as the key of a map which is used to group instances by one of its attributes is quite an uncommon use case. I think it would be better to just use the category attribute to group by category and have two maps: Map<String, List<Foo>> to keep the groups and Map<String, Foo> to keep the aggregated price and amount, with the key being the category in both cases.



                              Besides this, this solution mutates the keys of the map after the entries are put into it. This is dangerous, because this could break the map. However, here I'm only mutating attributes of Foo that don't participate in neither hashCode nor equals Foo's implementation. I think that this risk is acceptable in this case, due to the unusuality of the requirement.







                              share|improve this answer














                              share|improve this answer



                              share|improve this answer








                              edited Aug 29 at 13:35

























                              answered Aug 28 at 15:29









                              Federico Peralta Schaffner

                              18.5k32862




                              18.5k32862




















                                  up vote
                                  0
                                  down vote













                                  I suggest you create a helper class, which will hold amount and price



                                  final class Pair 
                                  final int amount;
                                  final int price;

                                  Pair(int amount, int price)
                                  this.amount = amount;
                                  this.price = price;




                                  And then just collect list to map:



                                  List<Foo> list =//....;

                                  Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                                  foo -> new Pair(foo.getAmount(), foo.getPrice()),
                                  (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));





                                  share|improve this answer




















                                  • that is a good start, but not what the OP needs... read the comments under the question
                                    – Eugene
                                    Aug 28 at 13:06














                                  up vote
                                  0
                                  down vote













                                  I suggest you create a helper class, which will hold amount and price



                                  final class Pair 
                                  final int amount;
                                  final int price;

                                  Pair(int amount, int price)
                                  this.amount = amount;
                                  this.price = price;




                                  And then just collect list to map:



                                  List<Foo> list =//....;

                                  Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                                  foo -> new Pair(foo.getAmount(), foo.getPrice()),
                                  (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));





                                  share|improve this answer




















                                  • that is a good start, but not what the OP needs... read the comments under the question
                                    – Eugene
                                    Aug 28 at 13:06












                                  up vote
                                  0
                                  down vote










                                  up vote
                                  0
                                  down vote









                                  I suggest you create a helper class, which will hold amount and price



                                  final class Pair 
                                  final int amount;
                                  final int price;

                                  Pair(int amount, int price)
                                  this.amount = amount;
                                  this.price = price;




                                  And then just collect list to map:



                                  List<Foo> list =//....;

                                  Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                                  foo -> new Pair(foo.getAmount(), foo.getPrice()),
                                  (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));





                                  share|improve this answer












                                  I suggest you create a helper class, which will hold amount and price



                                  final class Pair 
                                  final int amount;
                                  final int price;

                                  Pair(int amount, int price)
                                  this.amount = amount;
                                  this.price = price;




                                  And then just collect list to map:



                                  List<Foo> list =//....;

                                  Map<Foo, Pair> categotyPrise = list.stream().collect(Collectors.toMap(foo -> foo,
                                  foo -> new Pair(foo.getAmount(), foo.getPrice()),
                                  (o, n) -> new Pair(o.amount + n.amount, o.price + n.price)));






                                  share|improve this answer












                                  share|improve this answer



                                  share|improve this answer










                                  answered Aug 28 at 13:05









                                  dehasi

                                  539615




                                  539615











                                  • that is a good start, but not what the OP needs... read the comments under the question
                                    – Eugene
                                    Aug 28 at 13:06
















                                  • that is a good start, but not what the OP needs... read the comments under the question
                                    – Eugene
                                    Aug 28 at 13:06















                                  that is a good start, but not what the OP needs... read the comments under the question
                                  – Eugene
                                  Aug 28 at 13:06




                                  that is a good start, but not what the OP needs... read the comments under the question
                                  – Eugene
                                  Aug 28 at 13:06










                                  up vote
                                  0
                                  down vote













                                  My take on a solution :)



                                  public static void main(String args) 
                                  List<Foo> foos = new ArrayList<>();
                                  foos.add(new Foo("A", 1, 10));
                                  foos.add(new Foo("A", 2, 10));
                                  foos.add(new Foo("A", 3, 10));
                                  foos.add(new Foo("B", 1, 10));
                                  foos.add(new Foo("C", 1, 10));
                                  foos.add(new Foo("C", 5, 10));

                                  List<Foo> summarized = new ArrayList<>();
                                  Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>()
                                  @Override
                                  public Foo apply(Foo t)
                                  Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
                                  Foo f;
                                  if (!fOpt.isPresent())
                                  f = new Foo(t.getCategory(), 0, 0);
                                  summarized.add(f);
                                  else
                                  f = fOpt.get();

                                  f.setAmount(f.getAmount() + t.getAmount());
                                  f.setPrice(f.getPrice() + t.getPrice());
                                  return f;

                                  ));
                                  System.out.println(collect);






                                  share|improve this answer


























                                    up vote
                                    0
                                    down vote













                                    My take on a solution :)



                                    public static void main(String args) 
                                    List<Foo> foos = new ArrayList<>();
                                    foos.add(new Foo("A", 1, 10));
                                    foos.add(new Foo("A", 2, 10));
                                    foos.add(new Foo("A", 3, 10));
                                    foos.add(new Foo("B", 1, 10));
                                    foos.add(new Foo("C", 1, 10));
                                    foos.add(new Foo("C", 5, 10));

                                    List<Foo> summarized = new ArrayList<>();
                                    Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>()
                                    @Override
                                    public Foo apply(Foo t)
                                    Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
                                    Foo f;
                                    if (!fOpt.isPresent())
                                    f = new Foo(t.getCategory(), 0, 0);
                                    summarized.add(f);
                                    else
                                    f = fOpt.get();

                                    f.setAmount(f.getAmount() + t.getAmount());
                                    f.setPrice(f.getPrice() + t.getPrice());
                                    return f;

                                    ));
                                    System.out.println(collect);






                                    share|improve this answer
























                                      up vote
                                      0
                                      down vote










                                      up vote
                                      0
                                      down vote









                                      My take on a solution :)



                                      public static void main(String args) 
                                      List<Foo> foos = new ArrayList<>();
                                      foos.add(new Foo("A", 1, 10));
                                      foos.add(new Foo("A", 2, 10));
                                      foos.add(new Foo("A", 3, 10));
                                      foos.add(new Foo("B", 1, 10));
                                      foos.add(new Foo("C", 1, 10));
                                      foos.add(new Foo("C", 5, 10));

                                      List<Foo> summarized = new ArrayList<>();
                                      Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>()
                                      @Override
                                      public Foo apply(Foo t)
                                      Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
                                      Foo f;
                                      if (!fOpt.isPresent())
                                      f = new Foo(t.getCategory(), 0, 0);
                                      summarized.add(f);
                                      else
                                      f = fOpt.get();

                                      f.setAmount(f.getAmount() + t.getAmount());
                                      f.setPrice(f.getPrice() + t.getPrice());
                                      return f;

                                      ));
                                      System.out.println(collect);






                                      share|improve this answer














                                      My take on a solution :)



                                      public static void main(String args) 
                                      List<Foo> foos = new ArrayList<>();
                                      foos.add(new Foo("A", 1, 10));
                                      foos.add(new Foo("A", 2, 10));
                                      foos.add(new Foo("A", 3, 10));
                                      foos.add(new Foo("B", 1, 10));
                                      foos.add(new Foo("C", 1, 10));
                                      foos.add(new Foo("C", 5, 10));

                                      List<Foo> summarized = new ArrayList<>();
                                      Map<Foo, List<Foo>> collect = foos.stream().collect(Collectors.groupingBy(new Function<Foo, Foo>()
                                      @Override
                                      public Foo apply(Foo t)
                                      Optional<Foo> fOpt = summarized.stream().filter(e -> e.getCategory().equals(t.getCategory())).findFirst();
                                      Foo f;
                                      if (!fOpt.isPresent())
                                      f = new Foo(t.getCategory(), 0, 0);
                                      summarized.add(f);
                                      else
                                      f = fOpt.get();

                                      f.setAmount(f.getAmount() + t.getAmount());
                                      f.setPrice(f.getPrice() + t.getPrice());
                                      return f;

                                      ));
                                      System.out.println(collect);







                                      share|improve this answer














                                      share|improve this answer



                                      share|improve this answer








                                      edited Aug 28 at 13:19

























                                      answered Aug 28 at 13:14









                                      KDM

                                      3,7011921




                                      3,7011921



























                                           

                                          draft saved


                                          draft discarded















































                                           


                                          draft saved


                                          draft discarded














                                          StackExchange.ready(
                                          function ()
                                          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f52058261%2fjava-stream-group-by-and-sum-multiple-fields%23new-answer', 'question_page');

                                          );

                                          Post as a guest













































































                                          Comments

                                          Popular posts from this blog

                                          What does second last employer means? [closed]

                                          List of Gilmore Girls characters

                                          Confectionery