Come faccio a ordinare un elenco in base a parametri diversi in tempi diversi


95

Ho una classe denominata Personcon più proprietà, ad esempio:

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

Molti Personoggetti vengono memorizzati in un file ArrayList<Person>. Voglio ordinare questo elenco in base a più parametri di ordinamento e di volta in volta diversi. Ad esempio, una volta potrei voler ordinare in ordine namecrescente e poi addressdiscendente, e un'altra volta solo iddiscendente.

E non voglio creare i miei metodi di ordinamento (cioè, voglio usare Collections.sort(personList, someComparator). Qual è la soluzione più elegante che ottiene questo risultato?

Risposte:


193

Penso che il tuo approccio enum sia fondamentalmente valido, ma le istruzioni switch necessitano davvero di un approccio più orientato agli oggetti. Tener conto di:

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 esempio di utilizzo (con un'importazione statica).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}

12
+1 uso intelligente delle enumerazioni. Mi piace l'elegante combinazione che fai con le enumerazioni, il "discendente" e il "Composito". Immagino che manchi il trattamento dei valori nulli, ma è facile aggiungerlo allo stesso modo di "discendente".
KLE

1
Molte belle risposte che forniscono spunti di riflessione. Poiché nessuna risposta si è distinta come alternativa chiara, accetterò questa perché mi piace l'eleganza, ma esorto chiunque visualizzi questa risposta a controllare anche gli altri approcci.
runaros

1
@TheLittleNaruto, il metodo di confronto restituisce un numero negativo se o2 è maggiore, uno positivo se o1 è maggiore e zero se sono uguali. Moltiplicando per -1 si inverte il risultato, che è l'idea di decrescere (l'opposto del solito ordine crescente), lasciandolo uguale a zero se fossero uguali.
Yishai

5
Nota che da Java 8 puoi usare comparator.reversed()per discendere e puoi usare comparator1.thenComparing(comparator2)per concatenare i comparatori.
GuiSim

1
@JohnBaum, se il primo comparatore restituisce un risultato diverso da zero, quel risultato viene restituito e il resto della catena non viene eseguito.
Yishai

26

Puoi creare comparatori per ciascuna delle proprietà che potresti voler ordinare e quindi provare "concatenamento comparatore" :-) in questo modo:

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

Probabilmente riceverai un avviso quando viene utilizzato (sebbene in JDK7 dovresti essere in grado di sopprimerlo).
Tom Hawtin - tackline

Anche questo mi piace. Potete fornire un esempio su come utilizzarlo con l'esempio fornito?
runaros

@runaros: utilizzo dei comparatori dalla risposta di KLE: Collections.sort (/ * Collection <Person> * / people, new ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Janus Troelsen

16

Un modo è creare un oggetto Comparatorche prenda come argomenti un elenco di proprietà in base alle quali ordinare, come mostra questo esempio.

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

Può quindi essere utilizzato nel codice, ad esempio in questo modo:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);

Sì. Se vuoi che il tuo codice sia molto generico, il tuo enum potrebbe specificare solo la proprietà da leggere (potresti usare la reflection per ottenere la proprietà usando il nome dell'enumerazione) e potresti specificare il resto con una seconda enum: ASC & DESC e forse un terzo (NULL_FIRST o NULL_LAST).
KLE

8

Un approccio potrebbe essere quello di comporre Comparators. Questo potrebbe essere un metodo di libreria (sono sicuro che esiste da qualche parte là fuori).

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

Uso:

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

In alternativa, nota che Collections.sortè un ordinamento stabile. Se le prestazioni non sono assolutamente cruciali, si ordina l'ordine secondario prima del primario.

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

Un approccio intelligente, tuttavia, potrebbe essere reso più generico, in modo tale da includere un numero variabile di comparatori, possibilmente zero?
runaros

compose(nameComparator, compose(addressComparator, idComparator))Sarebbe un po 'migliore se Java avesse metodi di estensione.
Tom Hawtin - tackline

4

I comparatori ti consentono di farlo in modo molto semplice e naturale. Puoi creare singole istanze di comparatori, nella tua stessa classe Person o in una classe Service associata alle tue necessità.
Esempi, utilizzando classi interne anonime:

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

Se ne hai molti, puoi anche strutturare il codice dei tuoi comparatori come preferisci. Ad esempio, potresti:

  • ereditare da un altro comparatore,
  • avere un CompositeComparator che aggrega alcuni comparatori esistenti
  • avere un NullComparator che gestisce i casi null, quindi delega a un altro comparatore
  • eccetera...

2

Penso che accoppiare i selezionatori alla classe Persona, come nella tua risposta, non sia una buona idea, perché accoppia il confronto (di solito guidato dal business) e l'oggetto modello per avvicinarsi l'uno all'altro. Ogni volta che vuoi cambiare / aggiungere qualcosa al selezionatore, devi toccare la classe della persona, che di solito è qualcosa che non vuoi fare.

L'utilizzo di un servizio o qualcosa di simile, che fornisce istanze di confronto, come quello proposto da KLE, sembra molto più flessibile ed estensibile.


Per quanto mi riguarda questo porta a un accoppiamento stretto perché in qualche modo la classe titolare dei comparatori DEVE conoscere la struttura dei dati dettagliata di una classe Persona (fondamentalmente quali campi della classe Persona confrontare) e se cambierai mai qualcosa nei campi Persone questo porta a rintracciarla stessa cambiamenti nella classe dei comparatori. Immagino che i comparatori di Persona dovrebbero far parte di una classe Persona. blog.sanaulla.info/2008/06/26/…
Stan

2

Il mio approccio è basato su quello di Yishai. Il divario principale è che non c'è modo di ordinare prima ascendente per un attributo e poi decrescente per un altro. Questo non può essere fatto con le enumerazioni. Per questo ho usato le lezioni. Poiché SortOrder dipende fortemente dal tipo che ho preferito implementarlo come classe interna di persona.

La classe "Person" con la classe interna "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 esempio per usare la classe Person e il suo 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


Quale sarebbe la complessità di questo tipo di concatenamento di comparatori? Stiamo essenzialmente ordinando ogni volta che colleghiamo i comparatori? Quindi facciamo un'operazione * log (n) per ogni comparatore?
John Baum

0

Recentemente ho scritto un comparatore per ordinare più campi all'interno di un record di stringa delimitato. Consente di definire il delimitatore, la struttura dei record e le regole di ordinamento (alcune delle quali sono specifiche del tipo). Puoi usarlo convertendo un record Person in una stringa delimitata.

Le informazioni richieste vengono inviate al Comparator stesso, a livello di codice o tramite un file XML.

XML viene convalidato da un file XSD incorporato nel pacchetto. Ad esempio, di seguito è riportato un layout record delimitato da tabulazioni con quattro campi (due dei quali ordinabili):

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

Dovresti quindi usarlo in java in questo modo:

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

La libreria può essere trovata qui:

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


0

Supponiamo che Coordinatesia presente una classe e che sia necessario ordinarla in entrambi i modi in base alla coordinata X e alla coordinata Y. Per questo sono necessari due comparatori differnet. Di seguito è riportato il campione

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
        };
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.