web-dev-qa-db-fra.com

Comment comparer des objets avec plusieurs champs

Supposons que certains objets ayant plusieurs champs puissent être comparés:

public class Person {

    private String firstName;
    private String lastName;
    private String age;

    /* Constructors */

    /* Methods */

}

Donc, dans cet exemple, lorsque vous demandez si:

a.compareTo(b) > 0

vous demandez peut-être si le nom de famille de a vient avant b, ou si a est plus vieux que b, etc ...

Quel est le moyen le plus propre de permettre une comparaison multiple entre ces types d’objets sans ajouter d’encombrement ou de surcharge inutile?

  • L'interface Java.lang.Comparable permet la comparaison avec un seul champ.
  • L’ajout de nombreuses méthodes de comparaison (à savoir compareByFirstName(), compareByAge(), etc ...) est encombré à mon sens.

Alors, quelle est la meilleure façon de s'y prendre?

183
Yuval Adam

Vous pouvez implémenter une Comparator qui compare deux objets Person et vous pouvez examiner autant de champs que vous le souhaitez. Vous pouvez insérer une variable dans votre comparateur qui lui indique le champ à comparer, bien qu'il serait probablement plus simple d'écrire plusieurs comparateurs.

69
Elie

Avec Java 8: 

Comparator.comparing((Person p)->p.firstName)
          .thenComparing(p->p.lastName)
          .thenComparingInt(p->p.age);

Si vous avez des méthodes d'accesseur:

Comparator.comparing(Person::getFirstName)
          .thenComparing(Person::getLastName)
          .thenComparingInt(Person::getAge);

Si une classe implémente Comparable, un tel comparateur peut être utilisé dans la méthode compareTo:

@Override
public int compareTo(Person o){
    return Comparator.comparing(Person::getFirstName)
              .thenComparing(Person::getLastName)
              .thenComparingInt(Person::getAge)
              .compare(this, o);
}
283
Display Name

Vous devez implémenter Comparable <Person>. En supposant que tous les champs ne soient pas nuls (par souci de simplicité), que l'âge est un entier, et que le classement par comparaison est premier, le dernier, age, la méthode compareTo est assez simple:

public int compareTo(Person other) {
    int i = firstName.compareTo(other.firstName);
    if (i != 0) return i;

    i = lastName.compareTo(other.lastName);
    if (i != 0) return i;

    return Integer.compare(age, other.age);
}
144
Steve Kuo

(from House of Code )

En désordre et alambiqué: le tri à 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 frappe, de maintenance et est sujet aux erreurs.

La méthode de réflexion: Tri avec BeanComparator

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

Collections.sort(pizzas, chain);  

Évidemment, ceci est plus concis, mais encore plus sujet aux erreurs car vous perdez votre référence directe aux champs en utilisant plutôt des chaînes. Désormais, si un champ est renommé, le compilateur ne signalera même pas un problème. De plus, comme cette solution utilise la réflexion, le tri est beaucoup plus lent.

Comment y arriver: trier avec la chaîne de comparaison 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(); 
        */  
    }  
});  

Ceci est bien meilleur, mais requiert le code de plaque de chaudière pour le cas d'utilisation le plus courant: les valeurs nulles doivent être évaluées moins par défaut. Pour les champs nuls, vous devez fournir une directive supplémentaire à Guava pour savoir quoi faire dans ce cas. C'est un mécanisme flexible si vous voulez faire quelque chose de spécifique, mais vous voulez souvent le cas par défaut (c'est-à-dire 1, a, b, z, null).

Tri 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 pour ComparisonChain de Guava, cette classe de bibliothèque trie facilement plusieurs champs, mais définit également le comportement par défaut des valeurs NULL (1, a, b, z, null). Cependant, vous ne pouvez rien spécifier d’autre, à moins que vous ne fournissiez votre propre comparateur.

Ainsi

En fin de compte, il en va de la saveur et du besoin de flexibilité (Guava’s ComparisonChain) par rapport à un code concis (Apache’s CompareToBuilder).

Méthode bonus

J'ai trouvé une solution de Nice qui combine plusieurs comparateurs par ordre de priorité sur CodeReview dans une 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 a déjà une utilité:

ComparatorUtils.chainedComparator (comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
55
Benny Bottema

@Patrick Pour trier plusieurs champs consécutivement, essayez ComparatorChain

Une chaîne de comparaison est un comparateur qui encapsule un ou plusieurs comparateurs en séquence. La chaîne ComparatorChain appelle chaque comparateur en séquence jusqu'à ce que soit 1) chaque comparateur renvoie un résultat différent de zéro (et ce résultat est ensuite renvoyé), ou 2) la chaîne ComparatorChain est épuisée (et zéro est renvoyé). Ce type de tri est très similaire au tri multi-colonne en SQL et cette classe permet aux classes Java d’émuler ce type de comportement lors du tri d’une Liste.

Pour faciliter davantage le tri de type SQL, l'ordre d'un comparateur dans la liste peut> être inversé.

L'appel d'une méthode qui ajoute de nouveaux comparateurs ou modifie le tri ascendant/descendant après l'appel de la comparaison (Object, Object) donnera lieu à une UnsupportedOperationException. Veillez toutefois à ne pas modifier la liste de comparateurs sous-jacente ou le BitSet qui définit l'ordre de tri.

Les instances de ComparatorChain ne sont pas synchronisées. La classe n'est pas thread-safe au moment de la construction, mais elle est thread-safe pour effectuer plusieurs comparaisons une fois toutes les opérations d'installation terminées.

21
Nigel_V_Thomas

Une autre option que vous pouvez toujours envisager est Apache Commons. Il fournit beaucoup d'options.

import org.Apache.commons.lang3.builder.CompareToBuilder;

Ex:

public int compare(Person a, Person b){

   return new CompareToBuilder()
     .append(a.getName(), b.getName())
     .append(a.getAddress(), b.getAddress())
     .toComparison();
}
18
Xeroiris

Vous pouvez également consulter Enum qui implémente Comparator.

http://tobega.blogspot.com/2008/05/beautiful-enums.html

par exemple.

Collections.sort(myChildren, Child.Order.ByAge.descending());
11
Boune

Pour ceux qui sont en mesure d’utiliser l’API de streaming Java 8, il existe une approche plus précise qui est bien documentée ici: Lambdas and sorting

Je cherchais l'équivalent du C # LINQ:

.ThenBy(...)

J'ai trouvé le mécanisme dans Java 8 sur le comparateur:

.thenComparing(...)

Donc, voici l'extrait qui illustre l'algorithme.

    Comparator<Person> comparator = Comparator.comparing(person -> person.name);
    comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));

Consultez le lien ci-dessus pour une méthode plus simple et une explication sur la façon dont l'inférence de type de Java rend la définition un peu plus complexe en comparaison de LINQ.

Voici le test unitaire complet pour référence:

@Test
public void testChainedSorting()
{
    // Create the collection of people:
    ArrayList<Person> people = new ArrayList<>();
    people.add(new Person("Dan", 4));
    people.add(new Person("Andi", 2));
    people.add(new Person("Bob", 42));
    people.add(new Person("Debby", 3));
    people.add(new Person("Bob", 72));
    people.add(new Person("Barry", 20));
    people.add(new Person("Cathy", 40));
    people.add(new Person("Bob", 40));
    people.add(new Person("Barry", 50));

    // Define chained comparators:
    // Great article explaining this and how to make it even neater:
    // http://blog.jooq.org/2014/01/31/Java-8-friday-goodies-lambdas-and-sorting/
    Comparator<Person> comparator = Comparator.comparing(person -> person.name);
    comparator = comparator.thenComparing(Comparator.comparing(person -> person.age));

    // Sort the stream:
    Stream<Person> personStream = people.stream().sorted(comparator);

    // Make sure that the output is as expected:
    List<Person> sortedPeople = personStream.collect(Collectors.toList());
    Assert.assertEquals("Andi",  sortedPeople.get(0).name); Assert.assertEquals(2,  sortedPeople.get(0).age);
    Assert.assertEquals("Barry", sortedPeople.get(1).name); Assert.assertEquals(20, sortedPeople.get(1).age);
    Assert.assertEquals("Barry", sortedPeople.get(2).name); Assert.assertEquals(50, sortedPeople.get(2).age);
    Assert.assertEquals("Bob",   sortedPeople.get(3).name); Assert.assertEquals(40, sortedPeople.get(3).age);
    Assert.assertEquals("Bob",   sortedPeople.get(4).name); Assert.assertEquals(42, sortedPeople.get(4).age);
    Assert.assertEquals("Bob",   sortedPeople.get(5).name); Assert.assertEquals(72, sortedPeople.get(5).age);
    Assert.assertEquals("Cathy", sortedPeople.get(6).name); Assert.assertEquals(40, sortedPeople.get(6).age);
    Assert.assertEquals("Dan",   sortedPeople.get(7).name); Assert.assertEquals(4,  sortedPeople.get(7).age);
    Assert.assertEquals("Debby", sortedPeople.get(8).name); Assert.assertEquals(3,  sortedPeople.get(8).age);
    // Andi     : 2
    // Barry    : 20
    // Barry    : 50
    // Bob      : 40
    // Bob      : 42
    // Bob      : 72
    // Cathy    : 40
    // Dan      : 4
    // Debby    : 3
}

/**
 * A person in our system.
 */
public static class Person
{
    /**
     * Creates a new person.
     * @param name The name of the person.
     * @param age The age of the person.
     */
    public Person(String name, int age)
    {
        this.age = age;
        this.name = name;
    }

    /**
     * The name of the person.
     */
    public String name;

    /**
     * The age of the person.
     */
    public int age;

    @Override
    public String toString()
    {
        if (name == null) return super.toString();
        else return String.format("%s : %d", this.name, this.age);
    }
}
7
Luke Machowski
import com.google.common.collect.ComparisonChain;

/**
 * @author radler
 * Class Description ...
 */
public class Attribute implements Comparable<Attribute> {

    private String type;
    private String value;

    public String getType() { return type; }
    public void setType(String type) { this.type = type; }

    public String getValue() { return value; }
    public void setValue(String value) { this.value = value; }

    @Override
    public String toString() {
        return "Attribute [type=" + type + ", value=" + value + "]";
    }

    @Override
    public int compareTo(Attribute that) {
        return ComparisonChain.start()
            .compare(this.type, that.type)
            .compare(this.value, that.value)
            .result();
    }

}
6
Ran Adler

Écrire une Comparator manuellement pour un tel cas d'utilisation est une solution terrible, OMI. Ces approches ponctuelles présentent de nombreux inconvénients: 

  • Aucune réutilisation de code. Violer SEC.
  • Plaque de cuisson.
  • Possibilité accrue d'erreurs.

Alors quelle est la solution?

D'abord un peu de théorie.

Notons la proposition "type A supporte la comparaison" par Ord A. (Du point de vue du programme, vous pouvez considérer Ord A comme un objet contenant une logique permettant de comparer deux As. Oui, exactement comme Comparator.) 

Maintenant, si Ord A et Ord B, leur (A, B) composite devrait également prendre en charge la comparaison. c'est-à-dire Ord (A, B). Si Ord A, Ord B et Ord C, alors Ord (A, B, C)

Nous pouvons étendre cet argument à l'arité arbitraire et dire:

Ord A, Ord B, Ord C, ..., Ord ZOrd (A, B, C, .., Z)

Appelons cette déclaration 1.

La comparaison des composites fonctionnera exactement comme vous l'avez décrit dans votre question: la première comparaison sera tentée en premier, puis la suivante, puis la suivante, etc.

C'est la première partie de notre solution. Maintenant la deuxième partie.

Si vous connaissez ce Ord A et savez comment transformer B en A (appelez cette fonction de transformation f), vous pouvez également avoir Ord B. Comment? Eh bien, lorsque les deux instances B doivent être comparées, vous devez d'abord les transformer en A à l'aide de f, puis appliquer Ord A.

Ici, nous mappons la transformation B → A à Ord A → Ord B. Ceci est connu sous le nom de mappage contravariant (ou comap pour faire court).

Ord A, (B → A)comap Ord B

Appelons cette déclaration 2.


Appliquons maintenant ceci à votre exemple. 

Vous avez un type de données nommé Person qui comprend trois champs de type String

  • Nous savons que Ord String. Par l'instruction 1, Ord (String, String, String)

  • Nous pouvons facilement écrire une fonction de Person à (String, String, String). (Vous n'avez qu'à renvoyer les trois champs.) Puisque nous connaissons Ord (String, String, String) et Person → (String, String, String), à l'aide de l'instruction 2, nous pouvons utiliser comap pour obtenir Ord Person

CQFD.


Comment implémenter tous ces concepts?

La bonne nouvelle est que vous n'êtes pas obligé. Il existe déjà une bibliothèque qui implémente toutes les idées décrites dans ce post. (Si vous êtes curieux de savoir comment cela est mis en œuvre, vous pouvez regarder sous le capot .)

Voici à quoi ressemblera le code:

Ord<Person> personOrd = 
 p3Ord(stringOrd, stringOrd, stringOrd).comap(
   new F<Person, P3<String, String, String>>() {
     public P3<String, String, String> f(Person x) {
       return p(x.getFirstName(), x.getLastname(), x.getAge());
     }
   }
 );

Explication:

  • stringOrd est un objet de type Ord<String>. Cela correspond à notre proposition initiale de "comparaison de supports".
  • p3Ord est une méthode qui prend Ord<A>, Ord<B>, Ord<C> et renvoie Ord<P3<A, B, C>>. Ceci correspond à la déclaration 1. ( P3 représente un produit à trois éléments. Le produit est un terme algébrique pour les composites.)
  • comap correspond à bien, comap.
  • F<A, B> représente une fonction de transformation A → B.
  • p est une méthode d'usine pour créer des produits.
  • L'expression entière correspond à la déclaration 2.

J'espère que cela pourra aider.

6
missingfaktor

Au lieu de méthodes de comparaison, vous pouvez définir simplement plusieurs types de sous-classes "Comparateur" dans la classe Personne. De cette façon, vous pouvez les transmettre aux méthodes de tri des collections standard.

4
Marc Novakowski

Je pense que ce serait plus déroutant si votre algorithme de comparaison était "intelligent". J'irais avec les nombreuses méthodes de comparaison que vous avez suggérées.

La seule exception pour moi serait l'égalité. Pour les tests unitaires, il m’a été utile de remplacer les .Equals (en .net) afin de déterminer si plusieurs champs sont égaux entre deux objets (et non si les références sont égales).

2
Michael Haren

S'il existe plusieurs manières pour un utilisateur de commander une personne, vous pouvez également avoir plusieurs constants Comparator s comme constantes. La plupart des opérations de tri et des collections triées prennent un comparateur en paramètre.

2
sblundy

Il est facile de comparer deux objets avec la méthode hashcode en Java`

public class Sample{

  String a=null;
  String b=null;

  public Sample(){
      a="s";
      b="a";
  }
  public Sample(String a,String b){
      this.a=a;
      this.b=b;
  }
  public static void main(String args[]){
      Sample f=new Sample("b","12");
      Sample s=new Sample("b","12");
      //will return true
      System.out.println((s.a.hashCode()+s.b.hashCode())==(f.a.hashCode()+f.b.hashCode()));

      //will return false
      Sample f=new Sample("b","12");
      Sample s=new Sample("b","13");
      System.out.println((s.a.hashCode()+s.b.hashCode())==(f.a.hashCode()+f.b.hashCode()));

}
1
Exodus
//here threshold,buyRange,targetPercentage are three keys on that i have sorted my arraylist 
final Comparator<BasicDBObject> 

    sortOrder = new Comparator<BasicDBObject>() {
                    public int compare(BasicDBObject e1, BasicDBObject e2) {
                        int threshold = new Double(e1.getDouble("threshold"))
                        .compareTo(new Double(e2.getDouble("threshold")));
                        if (threshold != 0)
                            return threshold;

                        int buyRange = new Double(e1.getDouble("buyRange"))
                        .compareTo(new Double(e2.getDouble("buyRange")));
                        if (buyRange != 0)
                            return buyRange;

                        return (new Double(e1.getDouble("targetPercentage")) < new Double(
                                e2.getDouble("targetPercentage")) ? -1 : (new Double(
                                        e1.getDouble("targetPercentage")) == new Double(
                                                e2.getDouble("targetPercentage")) ? 0 : 1));
                    }
                };
                Collections.sort(objectList, sortOrder);
1
Pradeep Singh
//Following is the example in jdk 1.8
package com;
import Java.util.ArrayList;
import Java.util.Comparator;
import Java.util.List;

class User {
    private String firstName;
    private String lastName;
    private Integer age;

    public Integer getAge() {
        return age;
    }

    public User setAge(Integer age) {
        this.age = age;
        return this;
    }

    public String getFirstName() {
        return firstName;
    }

    public User setFirstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public String getLastName() {
        return lastName;
    }

    public User setLastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

}

public class MultiFieldsComparision {

    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();

        User u1 = new User().setFirstName("Pawan").setLastName("Singh").setAge(38);
        User u2 = new User().setFirstName("Pawan").setLastName("Payal").setAge(37);
        User u3 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(60);
        User u4 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(43);
        User u5 = new User().setFirstName("Pawan").setLastName("Chamoli").setAge(44);
        User u6 = new User().setFirstName("Pawan").setLastName("Singh").setAge(5);

        users.add(u1);
        users.add(u2);
        users.add(u3);
        users.add(u4);
        users.add(u5);
        users.add(u6);

        System.out.println("****** Before Sorting ******");

        users.forEach(user -> {
            System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge());
        });

        System.out.println("****** Aftre Sorting ******");

        users.sort(
                Comparator.comparing(User::getFirstName).thenComparing(User::getLastName).thenComparing(User::getAge));

        users.forEach(user -> {
            System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge());
        });

    }

}
0
Pawan

Habituellement, je remplace ma méthode compareTo() de la sorte lorsque je dois effectuer un tri multiniveau.

public int compareTo(Song o) {
    // TODO Auto-generated method stub
    int comp1 = 10000000*(movie.compareTo(o.movie))+1000*(artist.compareTo(o.artist))+songLength;
    int comp2 = 10000000*(o.movie.compareTo(movie))+1000*(o.artist.compareTo(artist))+o.songLength;
    return comp1-comp2;
} 

Ici, la préférence est donnée au nom du film, puis à l'artiste et enfin à songLength. Vous devez simplement vous assurer que ces multiplicateurs sont suffisamment éloignés pour ne pas dépasser les frontières les uns des autres. 

0
jayanth

À partir de La réponse de Steve l'opérateur ternaire peut être utilisé:

public int compareTo(Person other) {
    int f = firstName.compareTo(other.firstName);
    int l = lastName.compareTo(other.lastName);
    return f != 0 ? f : l != 0 ? l : Integer.compare(age, other.age);
}
0
Gerold Broser

Si vous implémentez l'interface Comparable , vous devrez choisir une propriété simple à classer. Ceci est connu comme un ordre naturel. Pensez-y comme par défaut. Il est toujours utilisé quand aucun comparateur spécifique n'est fourni. Il s’agit généralement d’un nom, mais votre cas d’utilisation peut faire appel à quelque chose de différent. Vous êtes libre d'utiliser un nombre illimité d'autres comparateurs que vous pouvez fournir à différentes API de collections pour remplacer le classement naturel.

Notez également que généralement si a.compareTo (b) == 0, alors a.equals (b) == true. C'est bon sinon, mais il y a des effets secondaires à prendre en compte. Voir les excellents javadocs sur l'interface Comparable et vous y trouverez de nombreuses informations intéressantes.

0
Mark Renouf

Après le blog étant donné un bon exemple de comparateur enchaîné 

http://www.codejava.net/Java-core/collections/sorting-a-list-by-multiple-attributes-example

import Java.util.Arrays;
import Java.util.Comparator;
import Java.util.List;

/**
 * This is a chained comparator that is used to sort a list by multiple
 * attributes by chaining a sequence of comparators of individual fields
 * together.
 *
 */
public class EmployeeChainedComparator implements Comparator<Employee> {

    private List<Comparator<Employee>> listComparators;

    @SafeVarargs
    public EmployeeChainedComparator(Comparator<Employee>... comparators) {
        this.listComparators = Arrays.asList(comparators);
    }

    @Override
    public int compare(Employee emp1, Employee emp2) {
        for (Comparator<Employee> comparator : listComparators) {
            int result = comparator.compare(emp1, emp2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }
}

Comparateur appelant:

Collections.sort(listEmployees, new EmployeeChainedComparator(
                new EmployeeJobTitleComparator(),
                new EmployeeAgeComparator(),
                new EmployeeSalaryComparator())
        );
0
vaquar khan