Agrupar por múltiples nombres de campo en java 8

90

Encontré el código para agrupar los objetos por algún nombre de campo de POJO. A continuación se muestra el código para eso:

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

Y la salida es (que es correcta):

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

Pero, ¿y si quiero agrupar por varios campos? Obviamente, puedo pasar algo de POJO en el groupingBy()método después de implementar el equals()método en ese POJO, pero ¿hay alguna otra opción como que pueda agrupar por más de un campo del POJO dado?

Por ejemplo, aquí en mi caso, quiero agrupar por nombre y edad.

Mital Pritmani
fuente
1
Un truco consiste en generar una cadena única de todos los campos.
Marko Topolnik
3
Por cierto, mappingcomo recopilador posterior es redundante en el código que ha publicado.
Marko Topolnik
8
Es una solución rápida y sucia people.collect(groupingBy(p -> Arrays.asList(p.name, p.age))).
Misha

Respuestas:

163

Tienes algunas opciones aquí. Lo más simple es encadenar a tus coleccionistas:

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

Luego, para obtener una lista de personas de 18 años llamadas Fred, usaría:

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

Una segunda opción es definir una clase que represente la agrupación. Esto puede estar dentro de Person. Este código usa un recordpero podría ser fácilmente una clase (conequals y hashCodedefinido) en las versiones de Java antes de que se agregara JEP 359:

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

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

Entonces puedes usar:

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

y busca con

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

Finalmente, si no desea implementar su propio registro de grupo, muchos de los marcos de Java tienen una pairclase diseñada para este tipo de cosas. Por ejemplo: apache commons pair Si usa una de estas bibliotecas, puede hacer que la clave del mapa sea un par del nombre y la edad:

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

y recuperar con:

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

Personalmente, no veo mucho valor en las tuplas genéricas ahora que los registros están disponibles en el idioma, ya que los registros muestran mejor la intención y requieren muy poco código.

velocista
fuente
5
Function<T,U>también oculta la intención en este sentido --- pero no verá a nadie declarando su propia interfaz funcional para cada paso de mapeo; la intención ya está en el cuerpo lambda. Lo mismo ocurre con las tuplas: son excelentes como tipos de pegamento entre componentes API. Por cierto, las clases de casos de Scala son, en mi humilde opinión, una gran victoria en términos de concisión y exposición intencional.
Marko Topolnik
1
Sí, veo tu punto. Supongo (como siempre) que depende de cómo se usen. El ejemplo que di arriba, usando un par como clave de un mapa, es un buen ejemplo de cómo no hacerlo. No estoy muy familiarizado con Scala; tendré que empezar a aprenderlo a medida que escuche cosas buenas.
velocista
1
Imagínese ser capaz de declarar NameAgecomo una sola línea: case class NameAge { val name: String; val age: Int }--- y se obtiene equals, hashCodey toString!
Marko Topolnik
1
Agradable: otro elemento insertado en mi cola de "debe hacer". ¡Es FIFO desafortunadamente!
velocista
@sprinter El tipo en el primer fragmento de código no es correcto y debe cambiarse aMap<String, Map<Integer, List<Person>>> map
kasur
38

Aquí mira el código:

Simplemente puede crear una función y dejar que haga el trabajo por usted, ¡un estilo funcional!

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

Ahora puedes usarlo como mapa:

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

¡Salud!

Deepesh Rehi
fuente
2
Usé esta solución pero diferente. Función <Persona, Cadena> compositeKey = personRecord -> StringUtils.join (personRecord.getName (), personRecord.getAge ());
bpedroso
8

El groupingBymétodo tiene el primer parámetro Function<T,K>donde:

@param <T> el tipo de los elementos de entrada

@param <K>el tipo de claves

Si reemplazamos lambda con la clase anónima en su código, podemos ver algo de eso:

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

Ahora cambie el parámetro de salida <K>. En este caso, por ejemplo, utilicé una clase de par de org.apache.commons.lang3.tuple para agrupar por nombre y edad, pero puede crear su propia clase para filtrar grupos según lo necesite.

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, después de reemplazar con lambda back, el código se ve así:

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
fuente
¿Qué pasa con el uso List<String>?
Alex78191
6

Hola, simplemente puede concatenar su groupingByKeytal 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
fuente
2

Defina una clase para la definición clave en su 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

}

Ahora en tu código,

peopleByManyParams = people
            .collect(Collectors.groupingBy(p -> new KeyObj( p.age, p.other1, p.other2 ), Collectors.mapping((Person p) -> p, toList())));
sarveshseri
fuente
3
Eso es solo reinventar Ararys.asList()--- que es, por cierto, una buena opción para el caso de OP.
Marko Topolnik
Y también similar al Pairejemplo mencionado en el otro ejemplo, pero sin límite de argumento.
Benny Bottema
También necesitas hacer esto inmutable. (y calcular hashCode) una vez)
RobAu
2

Puede usar List como clasificador para muchos campos, pero necesita ajustar valores nulos en Opcional:

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
fuente
1

Necesitaba hacer un informe para una empresa de catering que sirve almuerzos para varios clientes. En otras palabras, el catering puede tener una o más empresas que reciben pedidos del catering, ¡y debe saber cuántos almuerzos debe producir cada día para todos sus clientes!

Solo para darme cuenta, no usé la clasificación, para no complicar demasiado este ejemplo.

Este es mi codigo:

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

Así es como hice la agrupación por múltiples campos branchCode y prdId, simplemente publíquelo para alguien que lo necesite

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

        }
    }

La salida es la siguiente:

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

Después de formatearlo:

[
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
fuente