113 votes

Collections.sort avec plusieurs champs

Je dispose d'une liste d'objets "Report" comportant trois champs (tous de type String).

ReportKey
StudentNumber
School

J'ai une sorte de code qui va comme

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

});

Pour une raison quelconque, je n'ai pas l'ordre trié. On a conseillé de mettre des espaces entre les champs, mais pourquoi ?

Vous voyez quelque chose d'anormal dans ce code ?

0 votes

S'agit-il de champs de longueur fixe ? Que se passe-t-il si record1.getReportKey() est "AB" et record1.getStudentNumber() est "CD", mais que record2.getReportKey() est "ABCD" ?

0 votes

Longueur fixe. Désolé, j'ai oublié de le mentionner.

0 votes

176voto

Plantface Points 863

(originellement de Façons de trier des listes d'objets en Java sur la base de plusieurs champs )

Code de travail original dans cette phrase

Utiliser les lambdas de Java 8 (ajouté le 10 avril 2019)

Java 8 résout joliment ce problème grâce aux lambdas (bien que Guava et Apache Commons puissent encore offrir plus de flexibilité) :

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

Grâce à @gaoagong's réponse ci-dessous .

Notez qu'un avantage ici est que les getters sont évalués paresseusement (ex. getSchool() n'est évaluée que si elle est pertinente).

Désordonné et alambiqué : Triage à la main

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

Cela nécessite beaucoup de saisie, de maintenance et est sujet à des erreurs. Le seul avantage est que getters ne sont invoquées que lorsqu'elles sont pertinentes.

La voie de la réflexion : Triage avec BeanComparator

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

Collections.sort(pizzas, chain);  

C'est évidemment plus concis, mais encore plus sujet aux erreurs car vous perdez votre référence directe aux champs en utilisant des chaînes à la place (pas de sécurité de type, auto-réfactorisations). Maintenant, si un champ est renommé, le compilateur ne signalera même pas de problème. De plus, comme cette solution utilise la réflexion, le tri est beaucoup plus lent.

Pour s'y rendre : Trier avec la chaîne de comparaison 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(); 
        */  
    }  
});  

C'est beaucoup mieux, mais cela nécessite un code passe-partout pour le cas d'utilisation le plus courant : les valeurs nulles doivent avoir une valeur moindre par défaut. Pour les champs nuls, vous devez fournir une directive supplémentaire à Guava sur ce qu'il faut faire dans ce cas. C'est un mécanisme flexible si vous voulez faire quelque chose de spécifique, mais souvent vous voulez le cas par défaut (c'est-à-dire 1, a, b, z, null).

Et comme indiqué dans les commentaires ci-dessous, ces getters sont tous évalués immédiatement pour chaque comparaison.

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

Comme ComparisonChain de Guava, cette classe de bibliothèque trie facilement sur plusieurs champs, mais définit également un comportement par défaut pour les valeurs nulles (c'est-à-dire 1, a, b, z, null). Cependant, vous ne pouvez pas non plus spécifier autre chose, sauf si vous fournissez votre propre comparateur.

Encore une fois, comme indiqué dans les commentaires ci-dessous, ces getters sont tous évalués immédiatement pour chaque comparaison.

Ainsi,

En fin de compte, c'est une question de goût et de besoin de flexibilité (ComparisonChain de Guava) par rapport à un code concis (CompareToBuilder d'Apache).

Méthode de bonus

J'ai trouvé une solution intéressante qui combine plusieurs comparateurs par ordre de priorité. sur CodeReview dans 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));
    }
}

Bien sûr, Apache Commons Collections dispose déjà d'un utilitaire pour cela :

ComparatorUtils.chainedComparator(comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));

155voto

Jason S Points 58434

Vous voyez quelque chose d'anormal dans ce code ?

Oui. Pourquoi additionnez-vous les trois champs avant de les comparer ?

Je ferais probablement quelque chose comme ceci : (en supposant que les champs sont dans l'ordre dans lequel vous souhaitez les trier)

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

0 votes

Veuillez préciser. Comment dois-je m'y prendre ? Merci.

0 votes

Bonjour, vous pouvez ajouter d'autres if (c == 0) ? Je ne sais pas si c'est correct, mais il semble que non, parce que si la première condition est remplie, il n'y a jamais la deuxième ou la troisième etc .

11 votes

Je ne pense pas que vous ayez compris a.compareTo(b) ; la convention veut qu'une valeur de 0 représente l'égalité, les nombres entiers négatifs représentent que a < b et les entiers positifs représentent que a > b para Comparable a y b .

45voto

ColinD Points 48573

Je ferais un comparateur en utilisant Goyave '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();
  }
}

27voto

gaoagong Points 88

Il s'agit d'une ancienne question et je ne vois pas d'équivalent pour Java 8. Voici un exemple pour ce cas spécifique.

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 + '\'' +
                   '}';
        }
    }
}

1 votes

comparing y thenComparing pour la victoire !

1 votes

Il demande une version minimale d'Android 24.

15voto

Jon Skeet Points 692016

Si vous voulez trier par clé de rapport, puis par numéro d'étudiant, puis par école, vous devez faire quelque chose comme ceci :

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

Cela suppose qu'aucune des valeurs ne peut être nulle, bien sûr - cela devient plus compliqué si vous devez autoriser des valeurs nulles pour le rapport, la clé du rapport, le numéro de l'étudiant ou l'école.

Pendant que vous pourrait J'ai réussi à faire fonctionner la version de concaténation de chaînes de caractères en utilisant des espaces, mais elle échouait toujours dans des cas étranges si vous aviez des données bizarres qui incluaient elles-mêmes des espaces, etc. Le code ci-dessus est le logique code que vous voulez... comparez d'abord par clé de rapport, puis ne vous préoccupez du numéro d'étudiant que si les clés de rapport sont les mêmes, etc.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X