Kolekcje.sort z wieloma polami

83

Mam listę obiektów „Raport” z trzema polami (wszystkie typu String) -

ReportKey
StudentNumber
School

Mam numer sortowania, który wygląda następująco-

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

});

Z jakiegoś powodu nie mam posortowanej kolejności. Zalecano wstawianie spacji między polami, ale dlaczego?

Czy widzisz coś złego w kodzie?

Milli Szabo
źródło
Czy są to pola o stałej długości? Co się stanie, jeśli parametr record1.getReportKey () to „AB”, a parametr record1.getStudentNumber () to „CD”, a parametr record2.getReportKey () to „ABCD”?
mellamokb
Poprawiona długość. Przepraszam zapomniałem wspomnieć.
Milli Szabo

Odpowiedzi:

138

Czy widzisz coś złego w kodzie?

Tak. Dlaczego dodajesz trzy pola do siebie, zanim je porównasz?

Prawdopodobnie zrobiłbym coś takiego: (zakładając, że pola są w kolejności, w jakiej chcesz je sortować)

@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.
źródło
Proszę rozwinąć. Jak mam to zrobić? Dzięki.
Milli Szabo
Cześć, możesz dodać więcej if (c == 0)? Nie wiem, czy jest poprawne, ale wydaje mi się, że nie, bo jeśli pierwszy warunek jest spełniony, nigdy nie wchodzi w drugi ani trzeci .. itd.
10
Myślę, że nie zrozumiałeś a.compareTo(b); Konwencja jest taka, że ​​wartość 0 reprezentuje równość, ujemne liczby całkowite to, a < ba dodatnie liczby całkowite reprezentują to a > bdla Comparable ai b.
Jason S
1
To nie działa. Próbowałem tego i nie działa. Działa tylko z jednymcompareTo
shimatai
110

(pierwotnie ze sposobów sortowania list obiektów w Javie na podstawie wielu pól )

Oryginalny działający kod w tym istocie

Korzystanie z lambda Java 8 (dodano 10 kwietnia 2019 r.)

Java 8 ładnie rozwiązuje to za pomocą lambda (chociaż Guava i Apache Commons mogą nadal oferować większą elastyczność):

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

Dzięki poniższej odpowiedzi @ gaoagong .

Zwróć uwagę, że jedną z zalet jest to, że metody pobierające są oceniane leniwie (np. Są oceniane getSchool()tylko wtedy, gdy są istotne).

Niechlujny i zawiły: sortowanie ręczne

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

Wymaga to dużo pisania, konserwacji i jest podatne na błędy. Jedyną zaletą jest to, że metody pobierające są wywoływane tylko wtedy, gdy są istotne.

Odblaskowy sposób: sortowanie za pomocą BeanComparator

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

Collections.sort(pizzas, chain);  

Oczywiście jest to bardziej zwięzłe, ale jeszcze bardziej podatne na błędy, ponieważ tracisz bezpośrednie odniesienie do pól, używając zamiast tego ciągów znaków (brak bezpieczeństwa typów, autorefaktoryzacja). Teraz, jeśli nazwa pola zostanie zmieniona, kompilator nawet nie zgłosi problemu. Co więcej, ponieważ to rozwiązanie wykorzystuje odbicie, sortowanie przebiega znacznie wolniej.

Jak się tam dostać: sortowanie za pomocą porównywarki 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(); 
        */  
    }  
});  

Jest to o wiele lepsze, ale wymaga trochę kodu kotłowego dla najczęstszego przypadku użycia: wartości null powinny być domyślnie wyceniane mniej. W przypadku pól o wartości null musisz przekazać Guava dodatkową dyrektywę, co robić w takim przypadku. Jest to elastyczny mechanizm, jeśli chcesz zrobić coś konkretnego, ale często potrzebujesz domyślnej wielkości liter (np. 1, a, b, z, null).

Jak zauważono w komentarzach poniżej, wszystkie te metody pobierające są oceniane natychmiast dla każdego porównania.

Sortowanie za pomocą 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();  
    }  
});  

Podobnie jak CompareChain firmy Guava, ta klasa biblioteki łatwo sortuje na wielu polach, ale także definiuje domyślne zachowanie dla wartości null (np. 1, a, b, z, null). Jednak nie możesz też określić niczego innego, chyba że zapewnisz własny komparator.

Ponownie, jak zauważono w komentarzach poniżej, wszystkie te metody pobierające są oceniane natychmiast dla każdego porównania.

A zatem

Ostatecznie sprowadza się to do smaku i potrzeby elastyczności (Guava's CompareChain) w porównaniu do zwięzłego kodu (PorównajToBuilder Apache).

Metoda bonusowa

Znalazłem fajne rozwiązanie, które łączy wiele komparatorów w kolejności priorytetów w CodeReview w 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));
    }
}

Oczywiście Apache Commons Collections ma już do tego zastosowanie:

ComparatorUtils.chainedComparator (ComparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
Benny Bottema
źródło
Idealne rozwiązanie dla czystego kodu i również służy temu celowi
cryptonkid
świetne z java 8 lambda
frank
dzięki za świetną odpowiedź Benny. Mam scenariusz, w którym nieruchomość nie znajduje się bezpośrednio w moim obiekcie. ale istnieje obiekt zagnieżdżony. jak mam to zrobić? jak na przykład tutaj Collections.sort (reportList, Comparator.comparing (Report :: getReportKey) .thenComparing (Report :: getStudentNumber) .thenComparing (Report :: getSchool)); Wewnątrz obiektu raportu mam obiekt studenta, a wewnątrz obiektu studenta mam numer studenta. jak to uporządkujemy w takim przypadku? jakakolwiek pomoc byłaby doceniona.
Madhu Reddy
@MadhuReddy W przykładzie odwołania do metod są używane jako lambda, ale możesz po prostu podać odpowiednią lambdę, która zamiast tego zwróci odpowiednie pole zagnieżdżone.
Benny Bottema
44

Chciałbym zrobić komparator korzystając 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
źródło
20

To jest stare pytanie, więc nie widzę odpowiednika Java 8. Oto przykład dla tego konkretnego przypadku.

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
źródło
comparingi thenComparingpo zwycięstwo!
asgs
1
Prosi o minimalną wersję Androida 24.
viper
14

Jeśli chcesz posortować według klucza raportu, następnie numeru ucznia, a następnie szkoły, powinieneś zrobić coś takiego:

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

Zakłada to oczywiście, że żadna z wartości nie może być zerowa - sytuacja staje się bardziej skomplikowana, jeśli trzeba zezwolić na wartości zerowe dla raportu, klucza raportu, numeru ucznia lub szkoły.

Podczas mogli uzyskać wersję ciąg konkatenacji do pracy za pomocą spacji, to jeszcze nie w dziwnych przypadków, jeśli miał dane nieparzyste która sama łącznie ze spacjami itp Powyższy kod jest logiczne kod chcesz ... porównanie kluczem raportu, potem zawracaj sobie głowę numerem studenta tylko wtedy, gdy klucze raportu są takie same itp.

Jon Skeet
źródło
6
Chociaż nie ma „zła” w tym kodzie i rozumiem to. Wolę implementację Jasona, ponieważ wydaje się łatwiejsza do naśladowania, ponieważ ma tylko jedną instrukcję powrotu.
jzd
Kiedy próbuję użyć twojego kodu, nie mogę użyć compareTo()metody. Czy mógłbyś mi pomóc rozwiązać problem.
viper
@viper: No nie, bo nie jestem wykonawczych Comparable<T>, jestem wykonawczych Comparator<T>. Nie wiemy, co próbujesz osiągnąć, czego próbowałeś lub co poszło nie tak. Być może powinieneś zadać nowe pytanie, prawdopodobnie po przeprowadzeniu dalszych badań. (Być może już o to pytano.)
Jon Skeet,
7

Proponuję zastosować podejście Java 8 Lambda:

List<Report> reportList = new ArrayList<Report>();
reportList.sort(Comparator.comparing(Report::getRecord1).thenComparing(Report::getRecord2));
Zia Ul Mustafa
źródło
5

Sortowanie z wieloma polami w Javie8

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
źródło
empList.sort(comparing(Employee::getDesignation).thenComparing(Employee::getSalary));ten kod mi pomógł. Dzięki
Sumit Badaya
Prosi o użycie minimalnej wersji Androida 24.
viper
4

Jeśli numer ucznia jest numeryczny, nie będzie sortowany numerycznie, ale alfanumerycznie. Nie oczekuj

"2" < "11"

To będzie:

"11" < "2"
FrVaBe
źródło
To odpowiada na rzeczywiste pytanie, dlaczego wynik jest nieprawidłowy.
Florian F
3

Jeśli chcesz posortować najpierw na podstawie ReportKey, a następnie numeru ucznia, a następnie szkoły, musisz porównać każdy ciąg znaków, zamiast je łączyć. Twoja metoda może zadziałać, jeśli dopełnisz łańcuchy spacjami, tak aby każdy klucz ReportKey był tej samej długości i tak dalej, ale nie jest to naprawdę warte wysiłku. Zamiast tego po prostu zmień metodę porównania, aby porównać ReportKeys, jeśli compareTo zwraca 0, a następnie wypróbuj StudentNumber, a następnie School.

jzd
źródło
3

Użyj Comparatorinterfejsu z metodami wprowadzonymi w JDK1.8: comparingi thenComparing, lub bardziej konkretnymi metodami: comparingXXXi thenComparingXXX.

Na przykład, jeśli chcemy posortować listę osób najpierw według ich identyfikatora, następnie wieku, a następnie imienia:

            Comparator<Person> comparator = Comparator.comparingLong(Person::getId)
                    .thenComparingInt(Person::getAge)
                    .thenComparing(Person::getName);
            personList.sort(comparator);
puppylpg
źródło
0

Oto pełny przykład porównania 2 pól w obiekcie, jednego ciągu znaków i jednego elementu int, również przy użyciu narzędzia Collator do sortowania.

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

}

Wynik:

Wiśniewska 1039737

kosztuje 310831

kosztuje 310832

Koszty 1570019

miłość na żywo
źródło
0

Wiele powyższych odpowiedzi ma pola porównane w jednej metodzie porównawczej, która w rzeczywistości nie działa. Jest kilka odpowiedzi, chociaż z różnymi komparatorami zaimplementowanymi dla każdego pola, zamieszczam to, ponieważ ten przykład byłby znacznie jaśniejszy i łatwiejszy do zrozumienia, w które wierzę.

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



    }

}

WYNIK

Posortowana lista uczniów w kolejności malejącej to [Student [bornYear = 2018, bornMonth = null, bornDay = null], Student [bornYear = 2018, bornMonth = 12, bornDay = null], Student [bornYear = 2018, bornMonth = 12, bornDay = null], Student [bornYear = 2018, bornMonth = 11, bornDay = null], Student [bornYear = 2017, bornMonth = 9, bornDay = null], Student [bornYear = 2017, bornMonth = 8, bornDay = null], Student [ 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
źródło