Jak posortować listę według różnych parametrów w różnym czasie

95

Mam klasę o nazwie Personz wieloma właściwościami, na przykład:

public class Person {
    private int id;
    private String name, address;
    // Many more properties.
}

Wiele Person-obiektów jest przechowywanych w pliku ArrayList<Person>. Chcę posortować tę listę według wielu parametrów sortowania i różni się od czasu do czasu. Na przykład raz mógłbym chcieć posortować je namerosnąco, a potem addressmalejąco, a innym razem po prostu idmalejąco.

I nie chcę tworzyć własnych metod sortowania (tj. Chcę użyć Collections.sort(personList, someComparator). Jakie jest najbardziej eleganckie rozwiązanie, które to umożliwia?

runaros
źródło

Odpowiedzi:

193

Myślę, że twoje podejście wyliczeniowe jest w zasadzie rozsądne, ale instrukcje switch naprawdę wymagają podejścia bardziej zorientowanego obiektowo. Rozważać:

enum PersonComparator implements Comparator<Person> {
    ID_SORT {
        public int compare(Person o1, Person o2) {
            return Integer.valueOf(o1.getId()).compareTo(o2.getId());
        }},
    NAME_SORT {
        public int compare(Person o1, Person o2) {
            return o1.getFullName().compareTo(o2.getFullName());
        }};

    public static Comparator<Person> decending(final Comparator<Person> other) {
        return new Comparator<Person>() {
            public int compare(Person o1, Person o2) {
                return -1 * other.compare(o1, o2);
            }
        };
    }

    public static Comparator<Person> getComparator(final PersonComparator... multipleOptions) {
        return new Comparator<Person>() {
            public int compare(Person o1, Person o2) {
                for (PersonComparator option : multipleOptions) {
                    int result = option.compare(o1, o2);
                    if (result != 0) {
                        return result;
                    }
                }
                return 0;
            }
        };
    }
}

Przykład użycia (z importem statycznym).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}
Yishai
źródło
12
+1 inteligentne wykorzystanie wyliczeń. Podoba mi się elegancka kombinacja, którą robisz z wyliczeniami, „malejącymi” i „złożonymi”. Wydaje mi się, że brakuje traktowania wartości zerowych, ale łatwo jest dodać to samo, co „malejąco”.
KLE
1
Wiele miłych odpowiedzi dających do myślenia. Ponieważ żadna odpowiedź nie była jasną alternatywą, zaakceptuję tę, ponieważ lubię elegancję, ale zachęcam każdego, kto przegląda tę odpowiedź, do sprawdzenia również innych podejść.
runaros
1
@TheLittleNaruto, metoda porównania zwraca liczbę ujemną, jeśli o2 jest większe, dodatnią, jeśli o1 jest większe, i zero, jeśli są równe. Mnożenie przez -1 odwraca wynik, który jest ideą malejącej (przeciwieństwo zwykłej kolejności rosnącej), pozostawiając zero, jeśli są równe.
Yishai
5
Zauważ, że od wersji Java 8 możesz używać comparator.reversed()do zstępowania i możesz używać comparator1.thenComparing(comparator2)do łączenia w łańcuchy komparatorów.
GuiSim,
1
@JohnBaum, jeśli pierwszy komparator zwraca wynik niezerowy, ten wynik jest zwracany, a reszta łańcucha nie jest wykonywana.
Yishai
26

Możesz utworzyć komparatory dla każdej właściwości, którą chcesz posortować, a następnie wypróbować „łańcuch porównawczy” :-) w następujący sposób:

public class ChainedComparator<T> implements Comparator<T> {
    private List<Comparator<T>> simpleComparators; 
    public ChainedComparator(Comparator<T>... simpleComparators) {
        this.simpleComparators = Arrays.asList(simpleComparators);
    }
    public int compare(T o1, T o2) {
        for (Comparator<T> comparator : simpleComparators) {
            int result = comparator.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }
}
Tadeusz Kopeć
źródło
Prawdopodobnie otrzymasz ostrzeżenie, gdy zostanie użyte (chociaż w JDK7 powinieneś być w stanie je stłumić).
Tom Hawtin - tackline
Też mi się to podoba. Czy możesz podać próbkę, jak to wykorzystać w podanym przykładzie?
runaros
@runaros: Korzystanie z komparatorów z odpowiedzi KLE: Collections.sort (/ * Collection <Person> * / people, new ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Janus Troelsen
16

Jednym ze sposobów jest utworzenie Comparatorjako argumentów listy właściwości do sortowania, jak pokazano w tym przykładzie.

public class Person {
    private int id;
    private String name, address;

    public static Comparator<Person> getComparator(SortParameter... sortParameters) {
        return new PersonComparator(sortParameters);
    }

    public enum SortParameter {
        ID_ASCENDING, ID_DESCENDING, NAME_ASCENDING,
        NAME_DESCENDING, ADDRESS_ASCENDING, ADDRESS_DESCENDING
    }

    private static class PersonComparator implements Comparator<Person> {
        private SortParameter[] parameters;

        private PersonComparator(SortParameter[] parameters) {
            this.parameters = parameters;
        }

        public int compare(Person o1, Person o2) {
            int comparison;
            for (SortParameter parameter : parameters) {
                switch (parameter) {
                    case ID_ASCENDING:
                        comparison = o1.id - o2.id;
                        if (comparison != 0) return comparison;
                        break;
                    case ID_DESCENDING:
                        comparison = o2.id - o1.id;
                        if (comparison != 0) return comparison;
                        break;
                    case NAME_ASCENDING:
                        comparison = o1.name.compareTo(o2.name);
                        if (comparison != 0) return comparison;
                        break;
                    case NAME_DESCENDING:
                        comparison = o2.name.compareTo(o1.name);
                        if (comparison != 0) return comparison;
                        break;
                    case ADDRESS_ASCENDING:
                        comparison = o1.address.compareTo(o2.address);
                        if (comparison != 0) return comparison;
                        break;
                    case ADDRESS_DESCENDING:
                        comparison = o2.address.compareTo(o1.address);
                        if (comparison != 0) return comparison;
                        break;
                }
            }
            return 0;
        }
    }
}

Można go następnie użyć w kodzie, na przykład w ten sposób:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);
runaros
źródło
Tak. Jeśli chcesz, aby kod był bardzo ogólny, Twoje wyliczenie może określać tylko właściwość do odczytania (możesz użyć odbicia, aby uzyskać właściwość przy użyciu nazwy wyliczenia), a resztę możesz określić za pomocą drugiego wyliczenia: ASC i DESC, a prawdopodobnie trzeci (NULL_FIRST lub NULL_LAST).
KLE
8

Jednym podejściem byłoby skomponowanie Comparators. Może to być metoda biblioteczna (jestem pewien, że gdzieś tam istnieje).

public static <T> Comparator<T> compose(
    final Comparator<? super T> primary,
    final Comparator<? super T> secondary
) {
    return new Comparator<T>() {
        public int compare(T a, T b) {
            int result = primary.compare(a, b);
            return result==0 ? secondary.compare(a, b) : result;
        }
        [...]
    };
}

Posługiwać się:

Collections.sort(people, compose(nameComparator, addressComparator));

Alternatywnie, zauważ, że Collections.sortjest to typ stabilny. Jeśli wydajność nie jest absolutnie kluczowa, sortowanie będzie drugorzędne przed podstawowym.

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);
Tom Hawtin - haczyk
źródło
Czy jednak sprytne podejście można uczynić bardziej ogólnym, tak aby obejmowało zmienną liczbę komparatorów, być może w tym zero?
runaros
compose(nameComparator, compose(addressComparator, idComparator))Byłoby to trochę lepiej, gdyby Java miała metody rozszerzające.
Tom Hawtin - tackline
4

Komparatory pozwalają to zrobić bardzo łatwo i naturalnie. Możesz tworzyć pojedyncze wystąpienia komparatorów, w samej klasie Person lub w klasie Service powiązanej z twoimi potrzebami.
Przykłady, używając anonimowych klas wewnętrznych:

    public static final Comparator<Person> NAME_ASC_ADRESS_DESC
     = new Comparator<Person>() {
      public int compare(Person p1, Person p2) {
         int nameOrder = p1.getName().compareTo(p2.getName);
         if(nameOrder != 0) {
           return nameOrder;
         }
         return -1 * p1.getAdress().comparedTo(p2.getAdress());
         // I use explicit -1 to be clear that the order is reversed
      }
    };

    public static final Comparator<Person> ID_DESC
     = new Comparator<Person>() {
      public int compare(Person p1, Person p2) {
         return -1 * p1.getId().comparedTo(p2.getId());
         // I use explicit -1 to be clear that the order is reversed
      }
    };
    // and other comparator instances as needed... 

Jeśli masz wiele, możesz także dowolnie ustrukturyzować kod komparatorów . Na przykład możesz:

  • dziedziczyć z innego komparatora,
  • mają CompositeComparator, który agreguje niektóre istniejące komparatory
  • mają NullComparator, który obsługuje przypadki zerowe, a następnie deleguje do innego komparatora
  • itp...
KLE
źródło
2

Myślę, że połączenie sortowników z klasą Person, tak jak w twojej odpowiedzi, nie jest dobrym pomysłem, ponieważ łączy porównanie (zwykle napędzane biznesowo) i obiekt modelu tak, aby były blisko siebie. Za każdym razem, gdy chcesz coś zmienić / dodać sortownik, musisz dotknąć klasy osoby, co zwykle jest czymś, czego nie chcesz robić.

Korzystanie z usługi lub czegoś podobnego, które zapewnia wystąpienia komparatora, takie jak proponowane przez KLE, brzmi znacznie bardziej elastycznie i rozszerzalnie.

gia
źródło
Jeśli chodzi o mnie, prowadzi to do ścisłego powiązania, ponieważ w jakiś sposób klasa posiadacza komparatorów MUSI znać szczegółową strukturę danych klasy Person (w zasadzie, które pola klasy Person porównać) i jeśli kiedykolwiek zamierzasz coś zmienić w polach Person, prowadzi to do tego samego zmiany w klasie komparatorów. Sądzę, że komparatory Person powinny być częścią klasy Person. blog.sanaulla.info/2008/06/26/…
Stan
2

Moje podejście jest oparte na podejściu Yishai. Główna luka polega na tym, że nie ma możliwości sortowania najpierw rosnąco dla atrybutu, a następnie malejąco dla innego. Nie można tego zrobić za pomocą wyliczeń. Do tego użyłem klas. Ponieważ SortOrder silnie zależy od typu, wolałem zaimplementować go jako wewnętrzną klasę osoby.

Klasa „Osoba” z klasą wewnętrzną „SortOrder”:

import java.util.Comparator;

public class Person {
    private int id;
    private String firstName; 
    private String secondName;

    public Person(int id, String firstName, String secondName) {
        this.id = id;
        this.firstName = firstName;
        this.secondName = secondName;   
    }

    public abstract static class SortOrder implements Comparator<Person> {
        public static SortOrder PERSON_ID = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return Integer.valueOf(p1.getId()).compareTo(p2.getId());
            }
        };
        public static SortOrder PERSON_FIRST_NAME = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return p1.getFirstName().compareTo(p2.getFirstName());
            }
        };
        public static SortOrder PERSON_SECOND_NAME = new SortOrder() {
            public int compare(Person p1, Person p2) {
                return p1.getSecondName().compareTo(p2.getSecondName());
            }
        };

        public static SortOrder invertOrder(final SortOrder toInvert) {
            return new SortOrder() {
                public int compare(Person p1, Person p2) {
                    return -1 * toInvert.compare(p1, p2);
                }
            };
        }

        public static Comparator<Person> combineSortOrders(final SortOrder... multipleSortOrders) {
            return new Comparator<Person>() {
                public int compare(Person p1, Person p2) {
                    for (SortOrder personComparator: multipleSortOrders) {
                        int result = personComparator.compare(p1, p2);
                        if (result != 0) {
                            return result;
                        }
                    }
                    return 0;
                }
            };
        }
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getSecondName() {
        return secondName;
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();

        result.append("Person with id: ");
        result.append(id);
        result.append(" and firstName: ");
        result.append(firstName);
        result.append(" and secondName: ");
        result.append(secondName);
        result.append(".");

        return result.toString();
    }
}

Przykład użycia klasy Person i jej SortOrder:

import static multiplesortorder.Person.SortOrder.*;

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

import multiplesortorder.Person;

public class Application {

    public static void main(String[] args) {
        List<Person> listPersons = new ArrayList<Person>(Arrays.asList(
                 new Person(0, "...", "..."),
                 new Person(1, "...", "...")
             ));

         Collections.sort(listPersons, combineSortOrders(PERSON_FIRST_NAME, invertOrder(PERSON_ID)));

         for (Person p: listPersons) {
             System.out.println(p.toString());
         }
    }
}

oRUMOo

oRUMOo
źródło
Jaka byłaby złożoność tego rodzaju łączenia w łańcuchy komparatorów? Czy w zasadzie sortujemy za każdym razem, gdy łączymy w łańcuch komparatorów? Czyli wykonujemy operację * log (n) dla każdego komparatora?
John Baum
0

Niedawno napisałem komparator, aby posortować wiele pól w rozdzielanym rekordzie typu String. Pozwala zdefiniować separator, strukturę rekordu i reguły sortowania (niektóre z nich są specyficzne dla typu). Możesz tego użyć, konwertując rekord Person na rozdzielany ciąg.

Wymagane informacje są wysyłane do samego komparatora, programowo lub za pośrednictwem pliku XML.

XML jest weryfikowany przez osadzony w pakiecie plik XSD. Na przykład poniżej przedstawiono układ rekordów rozdzielany tabulatorami z czterema polami (z których dwa można sortować):

<?xml version="1.0" encoding="ISO-8859-1"?> 
<row xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <delimiter>&#009;</delimiter>

    <column xsi:type="Decimal">
        <name>Column One</name>
    </column>

    <column xsi:type="Integer">
        <name>Column Two</name>
    </column>

    <column xsi:type="String">
        <name>Column Three</name>
        <sortOrder>2</sortOrder>
        <trim>true</trim>
        <caseSensitive>false</caseSensitive>        
        <stripAccents>true</stripAccents>
    </column>

    <column xsi:type="DateTime">
        <name>Column Four</name>
        <sortOrder>1</sortOrder>
        <ascending>true</ascending>
        <nullLowSortOrder>true</nullLowSortOrder>
        <trim>true</trim>
        <pattern>yyyy-MM-dd</pattern>
    </column>

</row>

Możesz użyć tego w javie w następujący sposób:

Comparator<String> comparator = new RowComparator(
              new XMLStructureReader(new File("layout.xml")));

Bibliotekę można znaleźć tutaj:

http://sourceforge.net/projects/multicolumnrowcomparator/

Constantin
źródło
0

Załóżmy, że istnieje klasa Coordinatei trzeba ją posortować w obie strony według współrzędnej X i Y. Potrzebne są do tego dwa różne komparatory. Poniżej znajduje się próbka

class Coordinate
{

    int x,y;

    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }

    static Comparator<Coordinate> getCoordinateXComparator() {
        return new Comparator<Coordinate>() {

            @Override
            public int compare(Coordinate Coordinate1, Coordinate Coordinate2) {
                if(Coordinate1.x < Coordinate2.x)
                    return 1;
                else
                    return 0;
            }
            // compare using Coordinate x
        };
    }

    static Comparator<Coordinate> getCoordinateYComparator() {
        return new Comparator<Coordinate>() {

            @Override
            public int compare(Coordinate Coordinate1, Coordinate Coordinate2) {
                if(Coordinate1.y < Coordinate2.y)
                    return 1;
                else
                    return 0;
            }
            // compare using Coordinate y
        };
    }
}
ravi ranjan
źródło