Colecciones.ordenar con varios campos

83

Tengo una lista de objetos "Informe" con tres campos (Todo tipo de cadena) -

ReportKey
StudentNumber
School

Tengo un código de clasificación que dice:

Collections.sort(reportList, new Comparator<Report>() {

@Override
public int compare(final Report record1, final Report record2) {
      return (record1.getReportKey() + record1.getStudentNumber() + record1.getSchool())                      
        .compareTo(record2.getReportKey() + record2.getStudentNumber() + record2.getSchool());
      }

});

Por alguna razón, no tengo el orden ordenado. Se aconseja poner espacios entre campos, pero ¿por qué?

¿Ves algún problema con el código?

Milli Szabo
fuente
¿Son campos de longitud fija? ¿Qué sucede si record1.getReportKey () es "AB" y record1.getStudentNumber () es "CD", pero record2.getReportKey () es "ABCD"?
mellamokb
Longitud fija. Lo siento olvidé mencionar.
Milli Szabo
posible duplicado de la mejor manera de comparar objetos por múltiples campos?
Benny Bottema

Respuestas:

136

¿Ves algún problema con el código?

Si. ¿Por qué está sumando los tres campos antes de compararlos?

Probablemente haría algo como esto: (asumiendo que los campos están en el orden en el que desea ordenarlos)

@Override public int compare(final Report record1, final Report record2) {
    int c;
    c = record1.getReportKey().compareTo(record2.getReportKey());
    if (c == 0)
       c = record1.getStudentNumber().compareTo(record2.getStudentNumber());
    if (c == 0)
       c = record1.getSchool().compareTo(record2.getSchool());
    return c;
}
Jason S
fuente
Por favor elabora. ¿Cómo hago eso entonces? Gracias.
Milli Szabo
Hola, ¿puedes agregar más si (c == 0)? No sé si es correcto pero parece que no, porque si se cumple la primera condición nunca entra la segunda o la tercera .. etc ..
10
No creo que lo hayas entendido a.compareTo(b); la convención es que un valor de 0 representa la igualdad, los enteros negativos representan eso a < by los enteros positivos representan eso a > bpara Comparable ay b.
Jason S
1
No funciona. Lo intenté y no funciona. Funciona con solo unocompareTo
shimatai
107

(originalmente de Maneras de ordenar listas de objetos en Java en base a múltiples campos )

Código de trabajo original en esta esencia

Usando Java 8 lambda (agregado el 10 de abril de 2019)

Java 8 resuelve esto muy bien mediante lambda (aunque Guava y Apache Commons aún pueden ofrecer más flexibilidad):

Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
            .thenComparing(Report::getStudentNumber)
            .thenComparing(Report::getSchool));

Gracias a la respuesta de @gaoagong a continuación .

Tenga en cuenta que una ventaja aquí es que los captadores se evalúan de manera perezosa (por ejemplo, getSchool()solo se evalúa si es relevante).

Desordenado y complicado: clasificación a mano

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        int sizeCmp = p1.size.compareTo(p2.size);  
        if (sizeCmp != 0) {  
            return sizeCmp;  
        }  
        int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings);  
        if (nrOfToppingsCmp != 0) {  
            return nrOfToppingsCmp;  
        }  
        return p1.name.compareTo(p2.name);  
    }  
});  

Esto requiere mucho mecanografiado, mantenimiento y es propenso a errores. La única ventaja es que los captadores solo se invocan cuando son relevantes.

La forma reflexiva: clasificación con BeanComparator

ComparatorChain chain = new ComparatorChain(Arrays.asList(
   new BeanComparator("size"), 
   new BeanComparator("nrOfToppings"), 
   new BeanComparator("name")));

Collections.sort(pizzas, chain);  

Obviamente, esto es más conciso, pero aún más propenso a errores, ya que pierde su referencia directa a los campos al usar Strings en su lugar (sin seguridad de tipos, refactorizaciones automáticas). Ahora, si se cambia el nombre de un campo, el compilador ni siquiera informará un problema. Además, debido a que esta solución utiliza la reflexión, la clasificación es mucho más lenta.

Cómo llegar: clasificación con ComparisonChain de Google Guava

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result();  
        // or in case the fields can be null:  
        /* 
        return ComparisonChain.start() 
           .compare(p1.size, p2.size, Ordering.natural().nullsLast()) 
           .compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast()) 
           .compare(p1.name, p2.name, Ordering.natural().nullsLast()) 
           .result(); 
        */  
    }  
});  

Esto es mucho mejor, pero requiere algún código de placa de caldera para el caso de uso más común: los valores nulos deben valorarse menos por defecto. Para los campos nulos, debe proporcionar una directiva adicional a Guava sobre qué hacer en ese caso. Este es un mecanismo flexible si desea hacer algo específico, pero a menudo desea el caso predeterminado (es decir, 1, a, b, z, nulo).

Y como se indica en los comentarios a continuación, todos estos captadores se evalúan inmediatamente para cada comparación.

Ordenar con Apache Commons CompareToBuilder

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison();  
    }  
});  

Al igual que ComparisonChain de Guava, esta clase de biblioteca se clasifica fácilmente en varios campos, pero también define el comportamiento predeterminado para valores nulos (es decir, 1, a, b, z, nulo). Sin embargo, tampoco puede especificar nada más, a menos que proporcione su propio Comparador.

Nuevamente, como se indica en los comentarios a continuación, todos estos captadores se evalúan inmediatamente para cada comparación.

Así

En última instancia, todo se reduce al sabor y la necesidad de flexibilidad (ComparisonChain de Guava) frente al código conciso (CompareToBuilder de Apache).

Método de bonificación

Encontré una buena solución que combina múltiples comparadores en orden de prioridad en CodeReview en un MultiComparator:

class MultiComparator<T> implements Comparator<T> {
    private final List<Comparator<T>> comparators;

    public MultiComparator(List<Comparator<? super T>> comparators) {
        this.comparators = comparators;
    }

    public MultiComparator(Comparator<? super T>... comparators) {
        this(Arrays.asList(comparators));
    }

    public int compare(T o1, T o2) {
        for (Comparator<T> c : comparators) {
            int result = c.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }

    public static <T> void sort(List<T> list, Comparator<? super T>... comparators) {
        Collections.sort(list, new MultiComparator<T>(comparators));
    }
}

Por supuesto, Apache Commons Collections ya tiene una utilidad para esto:

ComparatorUtils.chainedComparator (comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
Benny Bottema
fuente
Solución perfecta para un código limpio y también sirve para el propósito
cryptonkid
genial con java 8 lambda
frank
gracias por la gran respuesta Benny. Tengo un escenario en el que la propiedad no está directamente dentro de mi objeto. pero hay un objeto anidado. ¿Cómo hago en ese caso? como por ejemplo, aquí Collections.sort (reportList, Comparator.comparing (Report :: getReportKey) .thenComparing (Report :: getStudentNumber) .thenComparing (Report :: getSchool)); Dentro del objeto de informe tengo un objeto de estudiante y luego dentro del objeto de estudiante tengo un número de estudiante. ¿Cómo solucionamos eso aquí en ese caso? Cualquier ayuda sería apreciada.
Madhu Reddy
@MadhuReddy En el ejemplo, las referencias a métodos se utilizan como lambda, pero puede proporcionar una lambda adecuada que devuelva el campo anidado apropiado.
Benny Bottema
44

Haría un comparador usando Guava 's ComparisonChain:

public class ReportComparator implements Comparator<Report> {
  public int compare(Report r1, Report r2) {
    return ComparisonChain.start()
        .compare(r1.getReportKey(), r2.getReportKey())
        .compare(r1.getStudentNumber(), r2.getStudentNumber())
        .compare(r1.getSchool(), r2.getSchool())
        .result();
  }
}
ColinD
fuente
20

Esta es una pregunta antigua, por lo que no veo un equivalente de Java 8. Aquí hay un ejemplo para este caso específico.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Compares multiple parts of the Report object.
 */
public class SimpleJava8ComparatorClass {

    public static void main(String[] args) {
        List<Report> reportList = new ArrayList<>();
        reportList.add(new Report("reportKey2", "studentNumber2", "school1"));
        reportList.add(new Report("reportKey4", "studentNumber4", "school6"));
        reportList.add(new Report("reportKey1", "studentNumber1", "school1"));
        reportList.add(new Report("reportKey3", "studentNumber2", "school4"));
        reportList.add(new Report("reportKey2", "studentNumber2", "school3"));

        System.out.println("pre-sorting");
        System.out.println(reportList);
        System.out.println();

        Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
            .thenComparing(Report::getStudentNumber)
            .thenComparing(Report::getSchool));

        System.out.println("post-sorting");
        System.out.println(reportList);
    }

    private static class Report {

        private String reportKey;
        private String studentNumber;
        private String school;

        public Report(String reportKey, String studentNumber, String school) {
            this.reportKey = reportKey;
            this.studentNumber = studentNumber;
            this.school = school;
        }

        public String getReportKey() {
            return reportKey;
        }

        public void setReportKey(String reportKey) {
            this.reportKey = reportKey;
        }

        public String getStudentNumber() {
            return studentNumber;
        }

        public void setStudentNumber(String studentNumber) {
            this.studentNumber = studentNumber;
        }

        public String getSchool() {
            return school;
        }

        public void setSchool(String school) {
            this.school = school;
        }

        @Override
        public String toString() {
            return "Report{" +
                   "reportKey='" + reportKey + '\'' +
                   ", studentNumber='" + studentNumber + '\'' +
                   ", school='" + school + '\'' +
                   '}';
        }
    }
}
gaoagong
fuente
comparingy thenComparingpor la victoria!
pregunta el
1
Solicita la versión mínima de Android 24.
viper
14

Si desea ordenar por clave de informe, luego número de estudiante, luego escuela, debe hacer algo como esto:

public class ReportComparator implements Comparator<Report>
{
    public int compare(Report r1, Report r2)
    {
        int result = r1.getReportKey().compareTo(r2.getReportKey());
        if (result != 0)
        {
            return result;
        }
        result = r1.getStudentNumber().compareTo(r2.getStudentNumber());
        if (result != 0)
        {
            return result;
        }
        return r1.getSchool().compareTo(r2.getSchool());
    }
}

Esto supone que ninguno de los valores puede ser nulo, por supuesto; se vuelve más complicado si necesita permitir valores nulos para el informe, clave de informe, número de estudiante o escuela.

Si bien podría hacer que la versión de concatenación de cadenas funcione usando espacios, aún fallaría en casos extraños si tuviera datos extraños que incluyan espacios, etc. El código anterior es el código lógico que desea ... compare primero por la clave de informe, luego solo moleste con el número de estudiante si las claves del informe son las mismas, etc.

Jon Skeet
fuente
6
Aunque no hay "mal" con este código y lo entiendo. Prefiero la implementación de Jason porque parece más fácil de seguir, ya que solo tiene una declaración de devolución.
jzd
Cuando intento usar su código, no puede usar el compareTo()método. ¿Podría ayudarme a resolver el problema?
víbora
@viper: Bueno, no, porque no estoy implementando Comparable<T>, estoy implementando Comparator<T>. No sabemos qué está tratando de lograr, o qué ha intentado, o qué salió mal. Quizás debería hacer una nueva pregunta, probablemente después de investigar más. (Es posible que ya se haya preguntado)
Jon Skeet
7

Sugiero utilizar el enfoque de Java 8 Lambda:

List<Report> reportList = new ArrayList<Report>();
reportList.sort(Comparator.comparing(Report::getRecord1).thenComparing(Report::getRecord2));
Zia Ul Mustafa
fuente
5

Ordenar con múltiples campos en Java8

package com.java8.chapter1;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import static java.util.Comparator.*;



 public class Example1 {

    public static void main(String[] args) {
        List<Employee> empList = getEmpList();


        // Before Java 8 
        empList.sort(new Comparator<Employee>() {

            @Override
            public int compare(Employee o1, Employee o2) {
                int res = o1.getDesignation().compareTo(o2.getDesignation());
                if (res == 0) {
                    return o1.getSalary() > o2.getSalary() ? 1 : o1.getSalary() < o2.getSalary() ? -1 : 0;
                } else {
                    return res;
                }

            }
        });
        for (Employee emp : empList) {
            System.out.println(emp);
        }
        System.out.println("---------------------------------------------------------------------------");

        // In Java 8

        empList.sort(comparing(Employee::getDesignation).thenComparing(Employee::getSalary));
        empList.stream().forEach(System.out::println);

    }
    private static List<Employee> getEmpList() {
        return Arrays.asList(new Employee("Lakshman A", "Consultent", 450000),
                new Employee("Chaitra S", "Developer", 250000), new Employee("Manoj PVN", "Developer", 250000),
                new Employee("Ramesh R", "Developer", 280000), new Employee("Suresh S", "Developer", 270000),
                new Employee("Jaishree", "Opearations HR", 350000));
    }
}

class Employee {
    private String fullName;
    private String designation;
    private double salary;

    public Employee(String fullName, String designation, double salary) {
        super();
        this.fullName = fullName;
        this.designation = designation;
        this.salary = salary;
    }

    public String getFullName() {
        return fullName;
    }

    public String getDesignation() {
        return designation;
    }

    public double getSalary() {
        return salary;
    }

    @Override
    public String toString() {
        return "Employee [fullName=" + fullName + ", designation=" + designation + ", salary=" + salary + "]";
    }

}
Lakshman Miani
fuente
empList.sort(comparing(Employee::getDesignation).thenComparing(Employee::getSalary));este código me ayudó. Gracias
Sumit Badaya
Pide usar la versión mínima de Android 24.
viper
4

Si StudentNumber es numérico, no se ordenará numérico sino alfanumérico. No esperes

"2" < "11"

será:

"11" < "2"
FrVaBe
fuente
Esto responde a la pregunta real de por qué el resultado es incorrecto.
Florian F
3

Si desea ordenar en función de ReportKey primero, luego Número de estudiante y luego Escuela, debe comparar cada Cadena en lugar de concatenarlas. Su método podría funcionar si rellena las cadenas con espacios para que cada ReportKey tenga la misma longitud, y así sucesivamente, pero realmente no vale la pena el esfuerzo. En su lugar, simplemente cambie el método de comparación para comparar ReportKeys, si compareTo devuelve 0, intente StudentNumber, luego School.

jzd
fuente
3

Utilice la Comparatorinterfaz con los métodos introducidos en JDK1.8: comparingy thenComparing, o métodos más concretos: comparingXXXy thenComparingXXX.

Por ejemplo, si queremos ordenar una lista de personas por su identificación primero, luego la edad, luego el nombre:

            Comparator<Person> comparator = Comparator.comparingLong(Person::getId)
                    .thenComparingInt(Person::getAge)
                    .thenComparing(Person::getName);
            personList.sort(comparator);
cachorro
fuente
0

Aquí hay un ejemplo completo que compara 2 campos en un objeto, uno String y uno int, también usando Collator para ordenar.

public class Test {

    public static void main(String[] args) {

        Collator myCollator;
        myCollator = Collator.getInstance(Locale.US);

        List<Item> items = new ArrayList<Item>();

        items.add(new Item("costrels", 1039737, ""));
        items.add(new Item("Costs", 1570019, ""));
        items.add(new Item("costs", 310831, ""));
        items.add(new Item("costs", 310832, ""));

        Collections.sort(items, new Comparator<Item>() {
            @Override
            public int compare(final Item record1, final Item record2) {
                int c;
                //c = record1.item1.compareTo(record2.item1); //optional comparison without Collator                
                c = myCollator.compare(record1.item1, record2.item1);
                if (c == 0) 
                {
                    return record1.item2 < record2.item2 ? -1
                            :  record1.item2 > record2.item2 ? 1
                            : 0;
                }
                return c;
            }
        });     

        for (Item item : items)
        {
            System.out.println(item.item1);
            System.out.println(item.item2);
        }       

    }

    public static class Item
    {
        public String item1;
        public int item2;
        public String item3;

        public Item(String item1, int item2, String item3)
        {
            this.item1 = item1;
            this.item2 = item2;
            this.item3 = item3;
        }       
    }

}

Salida:

costrels 1039737

cuesta 310831

cuesta 310832

Costos 1570019

vive el amor
fuente
0

Muchas de las respuestas anteriores tienen campos comparados en un método de comparación única que en realidad no funciona. Sin embargo, hay algunas respuestas con diferentes comparadores implementados para cada campo, estoy publicando esto porque este ejemplo sería mucho más claro y simple de entender, creo.

class Student{
    Integer bornYear;
    Integer bornMonth;
    Integer bornDay;
    public Student(int bornYear, int bornMonth, int bornDay) {

        this.bornYear = bornYear;
        this.bornMonth = bornMonth;
        this.bornDay = bornDay;
    }
    public Student(int bornYear, int bornMonth) {

        this.bornYear = bornYear;
        this.bornMonth = bornMonth;

    }
    public Student(int bornYear) {

        this.bornYear = bornYear;

    }
    public Integer getBornYear() {
        return bornYear;
    }
    public void setBornYear(int bornYear) {
        this.bornYear = bornYear;
    }
    public Integer getBornMonth() {
        return bornMonth;
    }
    public void setBornMonth(int bornMonth) {
        this.bornMonth = bornMonth;
    }
    public Integer getBornDay() {
        return bornDay;
    }
    public void setBornDay(int bornDay) {
        this.bornDay = bornDay;
    }
    @Override
    public String toString() {
        return "Student [bornYear=" + bornYear + ", bornMonth=" + bornMonth + ", bornDay=" + bornDay + "]";
    }


}
class TestClass
{       

    // Comparator problem in JAVA for sorting objects based on multiple fields 
    public static void main(String[] args)
    {
        int N,c;// Number of threads

        Student s1=new Student(2018,12);
        Student s2=new Student(2018,12);
        Student s3=new Student(2018,11);
        Student s4=new Student(2017,6);
        Student s5=new Student(2017,4);
        Student s6=new Student(2016,8);
        Student s7=new Student(2018);
        Student s8=new Student(2017,8);
        Student s9=new Student(2017,2);
        Student s10=new Student(2017,9);

        List<Student> studentList=new ArrayList<>();
        studentList.add(s1);
        studentList.add(s2);
        studentList.add(s3);
        studentList.add(s4);
        studentList.add(s5);
        studentList.add(s6);
        studentList.add(s7);
        studentList.add(s8);
        studentList.add(s9);
        studentList.add(s10);

        Comparator<Student> byMonth=new Comparator<Student>() {
            @Override
            public int compare(Student st1,Student st2) {
                if(st1.getBornMonth()!=null && st2.getBornMonth()!=null) {
                    return st2.getBornMonth()-st1.getBornMonth();
                }
                else if(st1.getBornMonth()!=null) {
                    return 1;
                }
                else {
                    return -1;
                }
        }};

        Collections.sort(studentList, new Comparator<Student>() {
            @Override
            public int compare(Student st1,Student st2) {
                return st2.getBornYear()-st1.getBornYear();
        }}.thenComparing(byMonth));

        System.out.println("The sorted students list in descending is"+Arrays.deepToString(studentList.toArray()));



    }

}

SALIDA

La lista de alumnos ordenados en orden descendente es [Student [bornYear = 2018, bornMonth = null, bornDay = null], Student [bornYear = 2018, bornMonth = 12, bornDay = null], Student [bornYear = 2018, bornMonth = 12, bornDay = null], Estudiante [bornYear = 2018, bornMonth = 11, bornDay = null], Student [bornYear = 2017, bornMonth = 9, bornDay = null], Student [bornYear = 2017, bornMonth = 8, bornDay = null], Estudiante [ bornYear = 2017, bornMonth = 6, bornDay = null], Student [bornYear = 2017, bornMonth = 4, bornDay = null], Student [bornYear = 2017, bornMonth = 2, bornDay = null], Student [bornYear = 2016, bornMonth = 8, bornDay = null]]

Naseer Mohammad
fuente