¿Cómo ordeno una lista por diferentes parámetros en diferentes tiempos?

95

Tengo una clase nombrada Personcon múltiples propiedades, por ejemplo:

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

Muchos Person-objetos se almacenan en un archivo ArrayList<Person>. Quiero ordenar esta lista por múltiples parámetros de ordenación y diferentes de vez en cuando. Por ejemplo, una vez podría querer ordenar nameascendiendo y luego addressdescendiendo, y otra vez simplemente iddescendiendo.

Y no quiero crear mis propios métodos de ordenación (es decir, quiero usar Collections.sort(personList, someComparator). ¿Cuál es la solución más elegante que lo logra?

runaros
fuente

Respuestas:

193

Creo que su enfoque de enumeración es básicamente sólido, pero las declaraciones de cambio realmente necesitan un enfoque más orientado a objetos. Considerar:

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 ejemplo de uso (con una importación estática).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}
Yishai
fuente
12
+1 uso inteligente de enumeraciones. Me gusta la elegante combinación que haces con las enumeraciones, el "descendente" y el "compuesto". Supongo que falta el tratamiento de valores nulos, pero es fácil de agregar de la misma manera que "descendente".
KLE
1
Muchas respuestas agradables que proporcionan alimento para el pensamiento. Dado que ninguna respuesta se destacó como la alternativa clara, aceptaré esta porque me gusta la elegancia, pero insto a cualquiera que vea esta respuesta a que compruebe también los otros enfoques.
runaros
1
@TheLittleNaruto, el método de comparación devuelve un número negativo si o2 es mayor, uno positivo si o1 es mayor y cero si son iguales. Multiplicar por -1 invierte el resultado, que es la idea de descender (lo opuesto al orden ascendente habitual), dejándolo como cero si fueran iguales.
Yishai
5
Tenga en cuenta que desde Java 8 se puede utilizar comparator.reversed()para descender y se puede utilizar comparator1.thenComparing(comparator2)para encadenar comparadores.
GuiSim
1
@JohnBaum, si el primer comparador devuelve un resultado distinto de cero, ese resultado se devuelve y el resto de la cadena no se ejecuta.
Yishai
26

Puede crear comparadores para cada una de las propiedades que desee ordenar y luego probar el "encadenamiento de comparadores" :-) así:

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
fuente
Probablemente recibirá una advertencia cuando se use (aunque en JDK7 debería poder suprimirlo).
Tom Hawtin - tackline
También me gusta esto. ¿Puede proporcionar una muestra de cómo usar esto con el ejemplo dado?
runaros
@runaros: Usando los comparadores de la respuesta de KLE: Collections.sort (/ * Collection <Person> * / people, new ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Janus Troelsen
16

Una forma es crear un Comparatorque tome como argumentos una lista de propiedades para ordenar, como muestra este ejemplo.

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

Luego se puede usar en código, por ejemplo, como este:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);
runaros
fuente
Si. Si desea que su código sea muy genérico, su enumeración podría especificar solo la propiedad a leer (podría usar la reflexión para obtener la propiedad usando el nombre de la enumeración), y podría especificar el resto con una segunda enumeración: ASC & DESC, y posiblemente un tercero (NULL_FIRST o NULL_LAST).
KLE
8

Un enfoque sería componer Comparators. Este podría ser un método de biblioteca (estoy seguro de que existe en algún lugar).

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

Utilizar:

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

Alternativamente, tenga en cuenta que Collections.sortes un tipo estable. Si el rendimiento no es absolutamente crucial, ordena ser el orden secundario antes que el primario.

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);
Tom Hawtin - tackline
fuente
Sin embargo, un enfoque inteligente, ¿podría hacerse más genérico, de modo que incluya un número variable de comparadores, posiblemente incluyendo cero?
runaros
compose(nameComparator, compose(addressComparator, idComparator))Eso se leería un poco mejor si Java tuviera métodos de extensión.
Tom Hawtin - tackline
4

Los comparadores le permiten hacerlo de manera muy fácil y natural. Puede crear instancias únicas de comparadores, ya sea en su propia clase Person o en una clase de Servicio asociada a su necesidad.
Ejemplos, usando clases internas anónimas:

    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 tiene muchos, también puede estructurar el código de sus comparadores de la forma que desee. Por ejemplo, podrías:

  • heredar de otro comparador,
  • tener un CompositeComparator que agregue algunos comparadores existentes
  • tener un NullComparator que maneja casos nulos, luego delega a otro comparador
  • etc ...
KLE
fuente
2

Creo que acoplar los clasificadores a la clase Person, como en su respuesta, no es una buena idea, porque empareja la comparación (generalmente impulsada por el negocio) y el objeto modelo para acercarse entre sí. Cada vez que desee cambiar / agregar algo al clasificador, debe tocar la clase de persona, que generalmente es algo que no desea hacer.

Usar un servicio o algo similar, que proporciona instancias de Comparator, como el propuesto por KLE, suena mucho más flexible y extensible.

gia
fuente
En cuanto a mí, esto conduce a un acoplamiento estrecho porque de alguna manera la clase de soporte de los comparadores TIENE que conocer la estructura de datos detallada de una clase de Persona (básicamente qué campos de la clase de Persona comparar) y si alguna vez va a cambiar algo en los campos de Personas, esto lleva a rastrear lo mismo cambios en la clase de comparadores. Supongo que los comparadores de personas deberían ser parte de una clase de personas. blog.sanaulla.info/2008/06/26/…
Stan
2

Mi enfoque se basa en el de Yishai. La principal brecha es que no hay forma de ordenar primero ascendente por un atributo y luego descendente por otro. Esto no se puede hacer con enumeraciones. Para eso usé clases. Debido a que SortOrder depende en gran medida del tipo, preferí implementarlo como una clase interna de persona.

La clase 'Persona' con la clase 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 ejemplo para usar la clase Person y su 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
fuente
¿Cuál sería la complejidad de este tipo de encadenamiento de comparadores? ¿Estamos esencialmente ordenando cada vez que encadenamos los comparadores? Entonces, ¿hacemos una operación * log (n) para cada comparador?
John Baum
0

Recientemente escribí un Comparador para ordenar varios campos dentro de un registro de cadena delimitado. Le permite definir el delimitador, la estructura del registro y las reglas de clasificación (algunas de las cuales son específicas del tipo). Puede usar esto convirtiendo un registro de persona en una cadena delimitada.

La información requerida se inserta en el propio Comparador, ya sea mediante programación o mediante un archivo XML.

XML es validado por un paquete de archivo XSD integrado. Por ejemplo, a continuación se muestra un diseño de registro delimitado por tabuladores con cuatro campos (dos de los cuales se pueden ordenar):

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

Luego usaría esto en java así:

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

La biblioteca se puede encontrar aquí:

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

Constantin
fuente
0

Supongamos que hay una clase Coordinatey uno tiene que ordenarla de ambas formas según la coordenada X y la coordenada Y. Para ello se necesitan dos comparadores diferentes. A continuación se muestra la muestra

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
fuente