Wie sortiere ich eine Liste nach verschiedenen Parametern zu unterschiedlichen Zeiten?

95

Ich habe eine Klasse Personmit mehreren Eigenschaften, zum Beispiel:

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

Viele Person-Objekte sind in einem gespeichert ArrayList<Person>. Ich möchte diese Liste nach mehreren Sortierparametern sortieren, die sich von Zeit zu Zeit unterscheiden. Zum Beispiel möchte ich vielleicht einmal nach nameaufsteigend und dann addressabsteigend sortieren und ein anderes Mal nur nach idabsteigend.

Und ich möchte keine eigenen Sortiermethoden erstellen (dh verwenden) Collections.sort(personList, someComparator). Was ist die eleganteste Lösung, mit der dies erreicht wird?

Runaros
quelle

Antworten:

193

Ich denke, Ihr Enum-Ansatz ist im Grunde genommen solide, aber die switch-Anweisungen benötigen wirklich einen objektorientierteren Ansatz. Erwägen:

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

Ein Anwendungsbeispiel (mit statischem Import).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}
Yishai
quelle
12
+1 intelligente Verwendung von Aufzählungen. Ich mag die elegante Kombination, die Sie mit den Enums, dem "Abstieg" und dem "Composite" machen. Ich denke, die Nullwertbehandlung fehlt, aber es ist einfach, den gleichen Weg wie "absteigend" hinzuzufügen.
KLE
1
Viele nette Antworten, die zum Nachdenken anregen. Da keine Antwort als klare Alternative herausstach, werde ich diese akzeptieren, weil ich die Eleganz mag, aber ich fordere jeden, der diese Antwort sieht, auf, auch die anderen Ansätze zu überprüfen.
Runaros
1
@TheLittleNaruto, die Vergleichsmethode gibt eine negative Zahl zurück, wenn o2 größer ist, eine positive, wenn o1 größer ist, und Null, wenn sie gleich sind. Das Multiplizieren mit -1 kehrt das Ergebnis um. Dies ist die Idee des Absteigens (das Gegenteil der üblichen aufsteigenden Reihenfolge), während es bei Null gleich bleibt, wenn sie gleich sind.
Yishai
5
Beachten Sie, dass Sie seit Java 8 comparator.reversed()zum Absteigen und comparator1.thenComparing(comparator2)zum Verketten von Komparatoren verwenden können.
GuiSim
1
@JohnBaum, wenn der erste Komparator ein Ergebnis ungleich Null zurückgibt, wird dieses Ergebnis zurückgegeben und der Rest der Kette wird nicht ausgeführt.
Yishai
26

Sie können Komparatoren für jede Eigenschaft erstellen, die Sie sortieren möchten, und dann "Komparatorverkettung" versuchen :-) wie folgt:

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 Kopec
quelle
Sie werden wahrscheinlich eine Warnung erhalten, wenn diese verwendet wird (obwohl Sie sie in JDK7 unterdrücken sollten).
Tom Hawtin - Tackline
Das gefällt mir auch. Können Sie ein Beispiel dafür geben, wie dies mit dem angegebenen Beispiel verwendet werden kann?
Runaros
@runaros: Verwenden der Komparatoren aus der Antwort von KLE: Collections.sort (/ * Collection <Person> * / people, neuer ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Janus Troelsen
16

Eine Möglichkeit besteht darin, eine ComparatorListe mit Eigenschaften zu erstellen , nach denen sortiert werden soll, wie in diesem Beispiel gezeigt.

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

Es kann dann beispielsweise wie folgt im Code verwendet werden:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);
Runaros
quelle
Ja. Wenn Ihr Code sehr allgemein sein soll, kann Ihre Aufzählung nur die zu lesende Eigenschaft angeben (Sie können Reflection verwenden, um die Eigenschaft unter Verwendung des Aufzählungsnamens abzurufen), und Sie können den Rest mit einer zweiten Aufzählung angeben: ASC & DESC und möglicherweise ein dritter (NULL_FIRST oder NULL_LAST).
KLE
8

Ein Ansatz wäre, Comparators zu komponieren . Dies könnte eine Bibliotheksmethode sein (ich bin sicher, dass sie irgendwo da draußen existiert).

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;
        }
        [...]
    };
}

Verwenden:

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

Beachten Sie alternativ, dass dies Collections.sorteine stabile Sortierung ist. Wenn die Leistung nicht unbedingt entscheidend ist, sortieren Sie die sekundäre Reihenfolge vor der primären.

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);
Tom Hawtin - Tackline
quelle
Könnte ein kluger Ansatz jedoch allgemeiner gestaltet werden, so dass er eine variable Anzahl von Komparatoren enthält, möglicherweise einschließlich Null?
Runaros
compose(nameComparator, compose(addressComparator, idComparator))Das würde etwas besser lesen, wenn Java Erweiterungsmethoden hätte.
Tom Hawtin - Tackline
4

Mit Comparators können Sie dies sehr einfach und natürlich tun. Sie können einzelne Instanzen von Komparatoren erstellen, entweder in Ihrer Person-Klasse selbst oder in einer Service-Klasse, die Ihrem Bedarf zugeordnet ist.
Beispiele für anonyme innere Klassen:

    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... 

Wenn Sie viele haben, können Sie Ihren Komparatorcode auch beliebig strukturieren. Zum Beispiel könnten Sie:

  • von einem anderen Komparator erben,
  • einen CompositeComparator haben, der einige vorhandene Komparatoren zusammenfasst
  • einen NullComparator haben, der Nullfälle behandelt und dann an einen anderen Komparator delegiert
  • etc...
KLE
quelle
2

Ich denke, die Kopplung der Sortierer an die Person-Klasse, wie in Ihrer Antwort, ist keine gute Idee, da dadurch der Vergleich (normalerweise geschäftsorientiert) und das Modellobjekt nahe beieinander gekoppelt werden. Jedes Mal, wenn Sie den Sortierer ändern / hinzufügen möchten, müssen Sie die Personenklasse berühren, was normalerweise nicht der Fall ist.

Die Verwendung eines Dienstes oder eines ähnlichen Dienstes, der Komparatorinstanzen bereitstellt, wie von KLE vorgeschlagen, klingt viel flexibler und erweiterbarer.

gia
quelle
Was mich betrifft, führt dies zu einer engen Kopplung, da die Komparatorhalterklasse irgendwie die detaillierte Datenstruktur einer Personenklasse kennen muss (im Grunde genommen, welche Felder der Personenklasse verglichen werden sollen) und wenn Sie jemals etwas in Personenfeldern ändern, führt dies dazu, dass dies nachverfolgt wird Änderungen in der Komparatorklasse. Ich denke, Personenkomparatoren sollten Teil einer Personenklasse sein. blog.sanaulla.info/2008/06/26/…
Stan
2

Mein Ansatz basiert auf dem von Yishai. Die Hauptlücke besteht darin, dass es keine Möglichkeit gibt, zuerst aufsteigend nach einem Attribut und danach absteigend nach einem anderen zu sortieren. Dies ist mit Aufzählungen nicht möglich. Dafür habe ich Klassen benutzt. Da der SortOrder stark vom Typ abhängt, habe ich es vorgezogen, ihn als innere Personenklasse zu implementieren.

Die Klasse 'Person' mit der inneren Klasse '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();
    }
}

Ein Beispiel für die Verwendung der Klasse Person und ihrer 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
quelle
Wie komplex wäre diese Art der Verkettung von Komparatoren? Sortieren wir im Wesentlichen jedes Mal, wenn wir die Komparatoren verketten? Also machen wir für jeden Komparator eine * log (n) -Operation?
John Baum
0

Ich habe kürzlich einen Komparator geschrieben, um mehrere Felder innerhalb eines begrenzten String-Datensatzes zu sortieren. Hier können Sie das Trennzeichen, die Datensatzstruktur und die Sortierregeln definieren (von denen einige typspezifisch sind). Sie können dies verwenden, indem Sie einen Personendatensatz in einen durch Trennzeichen getrennten String konvertieren.

Erforderliche Informationen werden entweder programmgesteuert oder über eine XML-Datei in den Komparator selbst übertragen.

XML wird durch eine in ein Paket eingebettete XSD-Datei validiert. Im Folgenden finden Sie beispielsweise ein tabulatorgetrenntes Datensatzlayout mit vier Feldern (von denen zwei sortierbar sind):

<?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>

Sie würden dies dann in Java wie folgt verwenden:

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

Bibliothek finden Sie hier:

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

Constantin
quelle
0

Angenommen, eine Klasse Coordinateist vorhanden und muss auf beide Arten nach X-Koordinate und Y-Koordinate sortiert werden. Dafür werden zwei verschiedene Komparatoren benötigt. Unten ist das Beispiel

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
quelle