Agrupar por vários nomes de campo em java 8

95

Encontrei o código para agrupar os objetos por algum nome de campo do POJO. Abaixo está o código para isso:

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);
    }
}

E a saída é (que está correta):

{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}]}

Mas e se eu quiser agrupar por vários campos? Obviamente, posso passar algum POJO no groupingBy()método após implementar o equals()método nesse POJO, mas existe alguma outra opção como se eu pudesse agrupar por mais de um campo do POJO fornecido?

Por exemplo, aqui no meu caso, quero agrupar por nome e idade.

Mital Pritmani
fonte
1
Um truque é apenas gerar uma string única de todos os campos.
Marko Topolnik
3
BTW mappingcomo um coletor downstream é redundante no código que você postou.
Marko Topolnik
8
Solução rápida e suja é people.collect(groupingBy(p -> Arrays.asList(p.name, p.age))).
Misha

Respostas:

170

Você tem algumas opções aqui. O mais simples é acorrentar seus coletores:

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

Então, para obter uma lista de pessoas de 18 anos chamadas Fred, você usaria:

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

Uma segunda opção é definir uma classe que represente o agrupamento. Isso pode estar dentro de Pessoa. Este código usa um, recordmas poderia facilmente ser uma classe (com equalse hashCodedefinida) nas versões do Java antes da adição do JEP 359:

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

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

Então você pode usar:

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

e pesquisar com

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

Finalmente, se você não deseja implementar seu próprio registro de grupo, muitos dos frameworks Java existentes têm uma pairclasse projetada para esse tipo de coisa. Por exemplo: apache commons pair Se você usar uma dessas bibliotecas, poderá transformar a chave do mapa em um par de nome e idade:

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

e recupere com:

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

Pessoalmente, não vejo muito valor em tuplas genéricas, agora que os registros estão disponíveis na linguagem, pois os registros exibem melhor a intenção e exigem muito pouco código.

velocista
fonte
5
Function<T,U>também esconde a intenção neste sentido --- mas você não verá ninguém declarando sua própria interface funcional para cada etapa de mapeamento; a intenção já está lá no corpo lambda. O mesmo acontece com as tuplas: elas são ótimas como tipos de cola entre os componentes da API. As classes de caso do BTW Scala são IMHO uma grande vitória em termos de concisão e exposição intencional.
Marko Topolnik
1
Sim, eu vejo seu ponto. Eu acho (como sempre) que depende de como eles são usados. O exemplo que dei acima - usando um par como chave de um mapa - é um bom exemplo de como não fazer isso. Não estou muito familiarizado com o Scala - terei de começar a aprendê-lo conforme ouço coisas boas.
velocista
1
Imagine ser capaz de declarar NameAgecomo um one-liner: case class NameAge { val name: String; val age: Int }--- e você começa equals, hashCodee toString!
Marko Topolnik
1
Legal - outro item empurrado para minha fila de 'tarefas obrigatórias'. Infelizmente é FIFO!
velocista
@sprinter O tipo no primeiro trecho de código não está correto e deve ser alterado paraMap<String, Map<Integer, List<Person>>> map
kasur
39

Aqui, olhe o código:

Você pode simplesmente criar uma Função e deixá-la fazer o trabalho por você, uma espécie de Estilo funcional!

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

Agora você pode usá-lo como um mapa:

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

Felicidades!

Deepesh Rehi
fonte
2
Usei essa solução, mas diferente. Função <Person, String> compositeKey = personRecord -> StringUtils.join (personRecord.getName (), personRecord.getAge ());
bpedroso
8

O groupingBymétodo tem o primeiro parâmetro Function<T,K>onde:

@param <T>o tipo dos elementos de entrada

@param <K>o tipo das chaves

Se substituirmos lambda pela classe anônima em seu código, poderemos ver algum tipo disso:

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

Agora altere o parâmetro de saída <K>. Neste caso, por exemplo, usei uma classe de pares de org.apache.commons.lang3.tuple para agrupar por nome e idade, mas você pode criar sua própria classe para filtrar grupos conforme necessário.

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

Finalmente, depois de substituir por lambda de volta, o código fica assim:

Map<Pair<Integer,String>, List<Person>> peopleByAgeAndName = people.collect(Collectors.groupingBy(p -> Pair.of(person.getAge(), person.getName()), Collectors.mapping((Person p) -> p, toList())));
Andrei Smirnov
fonte
Que tal usar List<String>?
Alex78191
7

Olá, você pode simplesmente concatenar seu groupingByKey, como

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();
}
Amandeep
fonte
2

Defina uma classe para definição de chave em seu grupo.

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

}

Agora em seu código,

peopleByManyParams = people
            .collect(Collectors.groupingBy(p -> new KeyObj( p.age, p.other1, p.other2 ), Collectors.mapping((Person p) -> p, toList())));
Sarveshseri
fonte
3
Isso é apenas reinventar Ararys.asList()--- o que é uma boa escolha para o caso da OP.
Marko Topolnik
E também semelhante ao Pairexemplo mencionado no outro exemplo, mas sem limite de argumento.
Benny Bottema
Além disso, você precisa tornar isso imutável. (e calcule o hashCode) uma vez)
RobAu
2

Você pode usar List como um classificador para muitos campos, mas precisa agrupar valores nulos em Optional:

Function<String, 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
fonte
1

Precisava fazer um relatório para uma empresa de catering que serve almoços para vários clientes. Ou seja, a restauração pode ter uma ou mais empresas que recebem encomendas da restauração e deve saber quantos almoços deve produzir todos os dias para todos os seus clientes!

Só para notar, não usei ordenação, para não complicar muito este exemplo.

Este é o meu código:

@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
fonte
0

Foi assim que fiz o agrupamento por vários campos branchCode e prdId, apenas postando para quem precisa

    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);

        }
    }

O resultado é o seguinte:

+++++++++++++++++++++++
[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}]

Depois de formatá-lo:

[
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}
]
Charudatta Joshi
fonte