136

I found the code for grouping the objects by some field name from POJO. Below is the code for that:

public class Temp {

    static class Person {

        private String name;
        private int age;
        private long salary;

        Person(String name, int age, long salary) {

            this.name = name;
            this.age = age;
            this.salary = salary;
        }

        @Override
        public String toString() {
            return String.format("Person{name='%s', age=%d, salary=%d}", name, age, salary);
        }
    }

    public static void main(String[] args) {
        Stream<Person> people = Stream.of(new Person("Paul", 24, 20000),
                new Person("Mark", 30, 30000),
                new Person("Will", 28, 28000),
                new Person("William", 28, 28000));
        Map<Integer, List<Person>> peopleByAge;
        peopleByAge = people
                .collect(Collectors.groupingBy(p -> p.age, Collectors.mapping((Person p) -> p, toList())));
        System.out.println(peopleByAge);
    }
}

And the output is (which is correct):

{24=[Person{name='Paul', age=24, salary=20000}], 28=[Person{name='Will', age=28, salary=28000}, Person{name='William', age=28, salary=28000}], 30=[Person{name='Mark', age=30, salary=30000}]}

But what if I want to group by multiple fields? I can obviously pass some POJO in groupingBy() method after implementing equals() method in that POJO but is there any other option like I can group by more than one fields from the given POJO?

E.g. here in my case, I want to group by name and age.

Mital Pritmani
  • 4,880
  • 8
  • 38
  • 39

9 Answers9

240

You have a few options here. The simplest is to chain your collectors:

Map<String, Map<Integer, List<Person>>> map = people
    .collect(Collectors.groupingBy(Person::getName,
        Collectors.groupingBy(Person::getAge));

Then to get a list of 18 year old people called Fred you would use:

map.get("Fred").get(18);

A second option is to define a class that represents the grouping. This can be inside Person. This code uses a record but it could just as easily be a class (with equals and hashCode defined) in versions of Java before JEP 359 was added:

class Person {
    record NameAge(String name, int age) { }

    public NameAge getNameAge() {
        return new NameAge(name, age);
    }
}

Then you can use:

Map<NameAge, List<Person>> map = people.collect(Collectors.groupingBy(Person::getNameAge));

and search with

map.get(new NameAge("Fred", 18));

Finally if you don't want to implement your own group record then many of the Java frameworks around have a pair class designed for this type of thing. For example: apache commons pair If you use one of these libraries then you can make the key to the map a pair of the name and age:

Map<Pair<String, Integer>, List<Person>> map =
    people.collect(Collectors.groupingBy(p -> Pair.of(p.getName(), p.getAge())));

and retrieve with:

map.get(Pair.of("Fred", 18));

Personally I don't really see much value in generic tuples now that records are available in the language as records display intent better and require very little code.

sprinter
  • 27,148
  • 6
  • 47
  • 78
  • 5
    `Function` also hides intent in this sense---but you won't see anyone declaring their own functional interface for each mapping step; the intent is already there in the lambda body. Same with tuples: they are great as glue types between API components. BTW Scala's *case classes* are IMHO a big win in terms of both conciseness and intent exposure. – Marko Topolnik Feb 06 '15 at 13:53
  • 1
    Yes I see your point. I guess (like always) it depends how they are used. The example I gave above - using a Pair as the key of a Map - is a good example of how not to do it. I'm not too familiar with Scala - will have to start learning it as I hear good things. – sprinter Feb 06 '15 at 13:59
  • 1
    Just imagine being able to declare `NameAge` as a one-liner: `case class NameAge { val name: String; val age: Int }`---and you get `equals`, `hashCode`, and `toString`! – Marko Topolnik Feb 06 '15 at 14:02
  • 1
    Nice -- another item pushed onto my 'must do' queue. It's FIFO unfortunately! – sprinter Feb 06 '15 at 14:04
  • @sprinter The type in the first code snippet is not correct and should be changed to `Map>> map` – kasur Feb 09 '16 at 11:47
  • Thanks! It was hard to understand for me since I didn't see that the chaining is inside the groupingBy method, not in the collect method: `groupingBy(Function, Collector)` I did a little helper to generate this groupingby: https://gist.github.com/pdemanget/18cd1df4d69c4ef9cfa64287511a14de – pdem Apr 15 '16 at 08:33
  • What about using `List`? – Alex78191 Nov 22 '18 at 16:46
  • I like the record example but even though we are on java 14 I'm afraid to use them (in production) because they are a "preview feature" and therefore "impermanent" – jkerak Dec 17 '20 at 20:39
  • @jkerak records will become a permanent feature in Java 16, due March 2021. The risk of using them is very low at this stage. – sprinter Dec 20 '20 at 21:42
  • that's great news. I look forward to using this new record feature in just a few short months then! – jkerak Dec 29 '20 at 05:09
55

Here look at the code:

You can simply create a Function and let it do the work for you, kind of functional Style!

Function<Person, List<Object>> compositeKey = personRecord ->
    Arrays.<Object>asList(personRecord.getName(), personRecord.getAge());

Now you can use it as a map:

Map<Object, List<Person>> map =
people.collect(Collectors.groupingBy(compositeKey, Collectors.toList()));

Cheers!

Farhan S.
  • 698
  • 8
  • 26
Deepesh Rehi
  • 883
  • 8
  • 31
  • 3
    I used this solution but different. Function compositeKey = personRecord -> StringUtils.join(personRecord.getName(), personRecord.getAge()); – bpedroso Mar 17 '19 at 05:54
  • @bpedroso This might be dangerous because if someone has name "Martin6" and age 6 he would end up in the same group as "Martin" with age 66 – Gandalf May 26 '23 at 15:52
15

Hi You can simply concatenate your groupingByKey such as

Map<String, List<Person>> peopleBySomeKey = people
                .collect(Collectors.groupingBy(p -> getGroupingByKey(p), Collectors.mapping((Person p) -> p, toList())));



//write getGroupingByKey() function
private String getGroupingByKey(Person p){
return p.getAge()+"-"+p.getName();
}
Anptk
  • 1,125
  • 2
  • 17
  • 28
Amandeep
  • 149
  • 1
  • 5
11

You can use List as a classifier for many fields, but you need wrap null values into Optional:

Function<Item, List> classifier = (item) -> List.of(
    item.getFieldA(),
    item.getFieldB(),
    Optional.ofNullable(item.getFieldC())
);

Map<List, List<Item>> grouped = items.stream()
    .collect(Collectors.groupingBy(classifier));
vinga
  • 1,912
  • 20
  • 33
9

The groupingBy method has the first parameter is Function<T,K> where:

@param <T> the type of the input elements

@param <K> the type of the keys

If we replace lambda with the anonymous class in your code, we can see some kind of that:

people.stream().collect(Collectors.groupingBy(new Function<Person, int>() {
            @Override
            public int apply(Person person) {
                return person.getAge();
            }
        }));

Just now change output parameter<K>. In this case, for example, I used a pair class from org.apache.commons.lang3.tuple for grouping by name and age, but you may create your own class for filtering groups as you need.

people.stream().collect(Collectors.groupingBy(new Function<Person, Pair<Integer, String>>() {
                @Override
                public YourFilter apply(Person person) {
                    return Pair.of(person.getAge(), person.getName());
                }
            }));

Finally, after replacing with lambda back, code looks like that:

Map<Pair<Integer,String>, List<Person>> peopleByAgeAndName = people.collect(Collectors.groupingBy(p -> Pair.of(person.getAge(), person.getName()), Collectors.mapping((Person p) -> p, toList())));
Community
  • 1
  • 1
2

Define a class for key definition in your group.

class KeyObj {

    ArrayList<Object> keys;

    public KeyObj( Object... objs ) {
        keys = new ArrayList<Object>();

        for (int i = 0; i < objs.length; i++) {
            keys.add( objs[i] );
        }
    }

    // Add appropriate isEqual() ... you IDE should generate this

}

Now in your code,

peopleByManyParams = people
            .collect(Collectors.groupingBy(p -> new KeyObj( p.age, p.other1, p.other2 ), Collectors.mapping((Person p) -> p, toList())));
sarveshseri
  • 13,738
  • 28
  • 47
2

I needed to make report for a catering firm which serves lunches for various clients. In other words, catering may have on or more firms which take orders from catering, and it must know how many lunches it must produce every single day for all it's clients !

Just to notice, I didn't use sorting, in order not to over complicate this example.

This is my code :

@Test
public void test_2() throws Exception {
    Firm catering = DS.firm().get(1);
    LocalDateTime ldtFrom = LocalDateTime.of(2017, Month.JANUARY, 1, 0, 0);
    LocalDateTime ldtTo = LocalDateTime.of(2017, Month.MAY, 2, 0, 0);
    Date dFrom = Date.from(ldtFrom.atZone(ZoneId.systemDefault()).toInstant());
    Date dTo = Date.from(ldtTo.atZone(ZoneId.systemDefault()).toInstant());

    List<PersonOrders> LON = DS.firm().getAllOrders(catering, dFrom, dTo, false);
    Map<Object, Long> M = LON.stream().collect(
            Collectors.groupingBy(p
                    -> Arrays.asList(p.getDatum(), p.getPerson().getIdfirm(), p.getIdProduct()),
                    Collectors.counting()));

    for (Map.Entry<Object, Long> e : M.entrySet()) {
        Object key = e.getKey();
        Long value = e.getValue();
        System.err.println(String.format("Client firm :%s, total: %d", key, value));
    }
}
dobrivoje
  • 848
  • 1
  • 9
  • 18
0

This is how I did grouping by multiple fields branchCode and prdId, Just posting it for someone in need

    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.LinkedList;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;

    /**
     *
     * @author charudatta.joshi
     */
    public class Product1 {

        public BigInteger branchCode;
        public BigInteger prdId;
        public String accountCode;
        public BigDecimal actualBalance;
        public BigDecimal sumActBal;
        public BigInteger countOfAccts;

        public Product1() {
        }

        public Product1(BigInteger branchCode, BigInteger prdId, String accountCode, BigDecimal actualBalance) {
            this.branchCode = branchCode;
            this.prdId = prdId;
            this.accountCode = accountCode;
            this.actualBalance = actualBalance;
        }

        public BigInteger getCountOfAccts() {
            return countOfAccts;
        }

        public void setCountOfAccts(BigInteger countOfAccts) {
            this.countOfAccts = countOfAccts;
        }

        public BigDecimal getSumActBal() {
            return sumActBal;
        }

        public void setSumActBal(BigDecimal sumActBal) {
            this.sumActBal = sumActBal;
        }

        public BigInteger getBranchCode() {
            return branchCode;
        }

        public void setBranchCode(BigInteger branchCode) {
            this.branchCode = branchCode;
        }

        public BigInteger getPrdId() {
            return prdId;
        }

        public void setPrdId(BigInteger prdId) {
            this.prdId = prdId;
        }

        public String getAccountCode() {
            return accountCode;
        }

        public void setAccountCode(String accountCode) {
            this.accountCode = accountCode;
        }

        public BigDecimal getActualBalance() {
            return actualBalance;
        }

        public void setActualBalance(BigDecimal actualBalance) {
            this.actualBalance = actualBalance;
        }

        @Override
        public String toString() {
            return "Product{" + "branchCode:" + branchCode + ", prdId:" + prdId + ", accountCode:" + accountCode + ", actualBalance:" + actualBalance + ", sumActBal:" + sumActBal + ", countOfAccts:" + countOfAccts + '}';
        }

        public static void main(String[] args) {
            List<Product1> al = new ArrayList<Product1>();
            System.out.println(al);
            al.add(new Product1(new BigInteger("01"), new BigInteger("11"), "001", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("11"), "002", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("12"), "003", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("12"), "004", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("12"), "005", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("01"), new BigInteger("13"), "006", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("11"), "007", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("11"), "008", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("12"), "009", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("12"), "010", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("12"), "011", new BigDecimal("10")));
            al.add(new Product1(new BigInteger("02"), new BigInteger("13"), "012", new BigDecimal("10")));
            //Map<BigInteger, Long> counting = al.stream().collect(Collectors.groupingBy(Product1::getBranchCode, Collectors.counting()));
            // System.out.println(counting);

            //group by branch code
            Map<BigInteger, List<Product1>> groupByBrCd = al.stream().collect(Collectors.groupingBy(Product1::getBranchCode, Collectors.toList()));
            System.out.println("\n\n\n" + groupByBrCd);

             Map<BigInteger, List<Product1>> groupByPrId = null;
              // Create a final List to show for output containing one element of each group
            List<Product> finalOutputList = new LinkedList<Product>();
            Product newPrd = null;
            // Iterate over resultant  Map Of List
            Iterator<BigInteger> brItr = groupByBrCd.keySet().iterator();
            Iterator<BigInteger> prdidItr = null;    



            BigInteger brCode = null;
            BigInteger prdId = null;

            Map<BigInteger, List<Product>> tempMap = null;
            List<Product1> accListPerBr = null;
            List<Product1> accListPerBrPerPrd = null;

            Product1 tempPrd = null;
            Double sum = null;
            while (brItr.hasNext()) {
                brCode = brItr.next();
                //get  list per branch
                accListPerBr = groupByBrCd.get(brCode);

                // group by br wise product wise
                groupByPrId=accListPerBr.stream().collect(Collectors.groupingBy(Product1::getPrdId, Collectors.toList()));

                System.out.println("====================");
                System.out.println(groupByPrId);

                prdidItr = groupByPrId.keySet().iterator();
                while(prdidItr.hasNext()){
                    prdId=prdidItr.next();
                    // get list per brcode+product code
                    accListPerBrPerPrd=groupByPrId.get(prdId);
                    newPrd = new Product();
                     // Extract zeroth element to put in Output List to represent this group
                    tempPrd = accListPerBrPerPrd.get(0);
                    newPrd.setBranchCode(tempPrd.getBranchCode());
                    newPrd.setPrdId(tempPrd.getPrdId());

                    //Set accCOunt by using size of list of our group
                    newPrd.setCountOfAccts(BigInteger.valueOf(accListPerBrPerPrd.size()));
                    //Sum actual balance of our  of list of our group 
                    sum = accListPerBrPerPrd.stream().filter(o -> o.getActualBalance() != null).mapToDouble(o -> o.getActualBalance().doubleValue()).sum();
                    newPrd.setSumActBal(BigDecimal.valueOf(sum));
                    // Add product element in final output list

                    finalOutputList.add(newPrd);

                }

            }

            System.out.println("+++++++++++++++++++++++");
            System.out.println(finalOutputList);

        }
    }

Output is as below:

+++++++++++++++++++++++
[Product{branchCode:1, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, Product{branchCode:1, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, Product{branchCode:1, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}, Product{branchCode:2, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, Product{branchCode:2, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, Product{branchCode:2, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}]

After Formatting it :

[
Product{branchCode:1, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, 
Product{branchCode:1, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, 
Product{branchCode:1, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}, 
Product{branchCode:2, prdId:11, accountCode:null, actualBalance:null, sumActBal:20.0, countOfAccts:2}, 
Product{branchCode:2, prdId:12, accountCode:null, actualBalance:null, sumActBal:30.0, countOfAccts:3}, 
Product{branchCode:2, prdId:13, accountCode:null, actualBalance:null, sumActBal:10.0, countOfAccts:1}
]
Harshil
  • 883
  • 1
  • 8
  • 25
Charudatta Joshi
  • 119
  • 1
  • 1
  • 5
0

Time to try new Java feature

Since jdk 14 record class is introduced, which fit this use case perfectly.
The benefit are:

  1. hashcode and equals are generated automatically.
  2. More readable and robust(compare to using List/concatenate field as key)

We just need to slightly modify the code to support multiple field grouping as below:

public class Temp {

    static class Person {

        private String name;
        private int age;
        private long salary;

        Person(String name, int age, long salary) {

            this.name = name;
            this.age = age;
            this.salary = salary;
        }

        @Override
        public String toString() {
            return String.format("Person{name='%s', age=%d, salary=%d}", name, age, salary);
        }
    }

    public static void main(String[] args) {
        Stream<Person> people = Stream.of(new Person("Paul", 24, 20000),
                new Person("Mark", 30, 30000),
                new Person("Will", 28, 28000),
                new Person("William", 28, 28000));
        record AgeAndSalary(int age, long salary) {
        }
        Map<AgeAndSalary, List<Person>> peopleByAgeAndSalary;
        peopleByAgeAndSalary = people
                .collect(Collectors.groupingBy(p -> new AgeAndSalary(p.age, p.salary), Collectors.mapping((Person p) -> p, toList())));
        System.out.println(peopleByAgeAndSalary);
    }
}
samabcde
  • 6,988
  • 2
  • 25
  • 41