Comment trier une liste par différents paramètres à différents moments

95

J'ai une classe nommée Personavec plusieurs propriétés, par exemple:

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

Un grand nombre d' Personobjets sont stockés dans un fichier ArrayList<Person>. Je veux trier cette liste par plusieurs paramètres de tri, et différents de temps en temps. Par exemple, je pourrais une fois vouloir trier par ordre namecroissant puis addressdécroissant, et une autre fois simplement par iddécroissant.

Et je ne veux pas créer mes propres méthodes de tri (c'est-à-dire que je veux utiliser Collections.sort(personList, someComparator). Quelle est la solution la plus élégante qui permet d'y parvenir?

runaros
la source

Réponses:

193

Je pense que votre approche enum est fondamentalement saine, mais les instructions switch ont vraiment besoin d'une approche plus orientée objet. Considérer:

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

Un exemple d'utilisation (avec une importation statique).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}
Yishai
la source
12
+1 utilisation intelligente des énumérations. J'aime la combinaison élégante que vous faites avec les énumérations, le "descendant" et le "Composite". Je suppose que le traitement des valeurs nulles est absent, mais il est facile d'ajouter de la même manière que "descendant".
KLE
1
Beaucoup de belles réponses qui donnent matière à réflexion. Étant donné qu'aucune réponse ne s'est imposée comme une alternative claire, je vais accepter celle-ci parce que j'aime l'élégance, mais j'exhorte tous ceux qui consultent cette réponse à vérifier également les autres approches.
runaros
1
@TheLittleNaruto, la méthode compare renvoie un nombre négatif si o2 est supérieur, un positif si o1 est supérieur et zéro s'ils sont égaux. Multiplier par -1 inverse le résultat, qui est l'idée de décroître (le contraire de l'ordre croissant habituel), tout en le laissant nul s'ils étaient égaux.
Yishai
5
Notez que depuis Java 8, vous pouvez utiliser comparator.reversed()pour la descente et vous pouvez utiliser comparator1.thenComparing(comparator2)pour chaîner les comparateurs.
GuiSim
1
@JohnBaum, si le premier comparateur renvoie un résultat non nul, ce résultat est retourné et le reste de la chaîne n'est pas exécuté.
Yishai
26

Vous pouvez créer des comparateurs pour chacune des propriétés que vous voudrez peut-être trier, puis essayer le "chaînage de comparateurs" :-) comme ceci:

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
la source
Vous allez probablement recevoir un avertissement lorsque cela est utilisé (bien que dans JDK7 vous devriez pouvoir le supprimer).
Tom Hawtin - tackline
J'aime ça aussi. Pouvez-vous fournir un exemple sur la façon de l'utiliser avec l'exemple donné?
runaros
@runaros: Utilisation des comparateurs de la réponse de KLE: Collections.sort (/ * Collection <Person> * / people, new ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Janus Troelsen
16

Une façon est de créer un Comparatorqui prend comme arguments une liste de propriétés à trier, comme le montre cet exemple.

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

Il peut ensuite être utilisé dans le code par exemple comme ceci:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);
runaros
la source
Oui. Si vous voulez que votre code soit très générique, votre énumération pourrait spécifier uniquement la propriété à lire (vous pouvez utiliser la réflexion pour obtenir la propriété en utilisant le nom enum), et vous pouvez spécifier le reste avec une deuxième énumération: ASC & DESC, et éventuellement un troisième (NULL_FIRST ou NULL_LAST).
KLE le
8

Une approche consisterait à composer l' Comparatorart. Cela pourrait être une méthode de bibliothèque (je suis sûr qu'elle existe quelque part).

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

Utilisation:

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

Sinon, notez qu'il Collections.sorts'agit d'un tri stable. Si les performances ne sont pas absolument cruciales, vous triez dans l'ordre secondaire avant le primaire.

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);
Tom Hawtin - Tacle
la source
Approche intelligente, cependant, pourrait-elle être rendue plus générique, de sorte qu'elle inclue un nombre variable de comparateurs, y compris éventuellement zéro?
runaros le
compose(nameComparator, compose(addressComparator, idComparator))Cela se lirait un peu mieux si Java avait des méthodes d'extension.
Tom Hawtin - tackline le
4

Les comparateurs vous permettent de le faire très facilement et naturellement. Vous pouvez créer des instances uniques de comparateurs, soit dans votre classe Person elle-même, soit dans une classe Service associée à votre besoin.
Exemples, utilisant des classes internes anonymes:

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

Si vous en avez beaucoup, vous pouvez également structurer le code de votre comparateur comme vous le souhaitez. Par exemple, vous pourriez:

  • hériter d'un autre comparateur,
  • avoir un CompositeComparator qui agrège certains comparateurs existants
  • avoir un NullComparator qui gère les cas nuls, puis délègue à un autre comparateur
  • etc...
KLE
la source
2

Je pense que coupler les trieurs à la classe Person, comme dans votre réponse, n'est pas une bonne idée, car cela couple la comparaison (généralement axée sur l'entreprise) et l'objet modèle pour se rapprocher l'un de l'autre. Chaque fois que vous souhaitez modifier / ajouter quelque chose au trieur, vous devez toucher la classe de personne, ce que vous ne voulez généralement pas faire.

Utiliser un service ou quelque chose de similaire, qui fournit des instances de comparateur, comme KLE proposé, semble beaucoup plus flexible et extensible.

gia
la source
En ce qui me concerne, cela conduit à un couplage étroit car d'une manière ou d'une autre, la classe de détenteurs de comparateurs DOIT connaître la structure de données détaillée d'une classe Person (essentiellement les champs de la classe Person à comparer) et si vous changez quelque chose dans les champs Persons, cela conduit à tracer la même chose. changements dans la classe des comparateurs. Je suppose que les comparateurs de personne devraient faire partie d'une classe de personne. blog.sanaulla.info/2008/06/26/…
Stan
2

Mon approche est basée sur celle de Yishai. Le principal écart est qu'il n'y a aucun moyen de trier d'abord par ordre croissant pour un attribut et ensuite par ordre décroissant pour un autre. Cela ne peut pas être fait avec des énumérations. Pour cela, j'ai utilisé des cours. Parce que le SortOrder dépend fortement du type, j'ai préféré l'implémenter en tant que classe interne de personne.

La classe 'Person' avec la classe interne '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();
    }
}

Un exemple d'utilisation de la classe Person et de son 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
la source
Quelle serait la complexité de ce type de chaînage de comparateurs? Sommes-nous essentiellement en train de trier à chaque fois que nous enchaînons les comparateurs? Nous faisons donc une opération * log (n) pour chaque comparateur?
John Baum
0

J'ai récemment écrit un comparateur pour trier plusieurs champs dans un enregistrement String délimité. Il vous permet de définir le délimiteur, la structure des enregistrements et les règles de tri (dont certaines sont spécifiques au type). Vous pouvez l'utiliser en convertissant un enregistrement Personne en une chaîne délimitée.

Les informations requises sont envoyées au comparateur lui-même, soit par programme, soit via un fichier XML.

XML est validé par un fichier XSD intégré au package. Par exemple, vous trouverez ci-dessous une mise en page d'enregistrement délimitée par des tabulations avec quatre champs (dont deux peuvent être triés):

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

Vous utiliseriez alors ceci en java comme ceci:

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

La bibliothèque peut être trouvée ici:

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

Constantin
la source
0

Supposons qu'une classe Coordinatesoit là et que l'on doive la trier dans les deux sens selon les coordonnées X et Y. Deux comparateurs différents sont nécessaires pour cela. Voici l'exemple

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
la source