Como faço para classificar uma lista por diferentes parâmetros em diferentes tempos

95

Tenho uma classe chamada Personcom várias propriedades, por exemplo:

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

Muitos Personobjetos -são armazenados em um ArrayList<Person>. Quero classificar esta lista por vários parâmetros de classificação e diferentes de vez em quando. Por exemplo, posso querer uma vez ordenar por ordem nameascendente e depois addressdecrescente, e outra vez apenas por ordem iddecrescente.

E não quero criar meus próprios métodos de classificação (ou seja, quero usar Collections.sort(personList, someComparator). Qual é a solução mais elegante que consegue isso?

runaros
fonte

Respostas:

193

Acho que sua abordagem enum é basicamente sólida, mas as instruções switch realmente precisam de uma abordagem mais orientada 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;
            }
        };
    }
}

Um exemplo de uso (com importação estática).

public static void main(String[] args) {
    List<Person> list = null;
    Collections.sort(list, decending(getComparator(NAME_SORT, ID_SORT)));
}
Yishai
fonte
12
+1 uso inteligente de enums. Eu gosto da combinação elegante que você faz com os enums, o "descendente" e o "Composto". Eu acho que o tratamento de valores nulos está faltando, mas é fácil adicionar da mesma forma que "decrescente".
KLE de
1
Muitas respostas agradáveis ​​que fornecem alimento para reflexão. Uma vez que nenhuma resposta se destacou como uma alternativa clara, vou aceitar esta porque gosto da elegância, mas peço a todos que estão vendo esta resposta que verifiquem também as outras abordagens.
runaros de
1
@TheLittleNaruto, o método de comparação retorna um número negativo se o2 for maior, um número positivo se o1 for maior e zero se eles forem iguais. Multiplicar por -1 inverte o resultado, que é a ideia de decrescente (o oposto da ordem crescente de costume), deixando-o como zero se eles fossem iguais.
Yishai
5
Observe que, desde o Java 8, você pode usar comparator.reversed()para decrescente e comparator1.thenComparing(comparator2)para encadear comparadores.
GuiSim
1
@JohnBaum, se o primeiro comparador retornar um resultado diferente de zero, esse resultado será retornado e o resto da cadeia não será executado.
Yishai
26

Você pode criar comparadores para cada uma das propriedades que deseja classificar e, em seguida, tentar "encadeamento de comparadores" :-) assim:

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
fonte
Você provavelmente receberá um aviso quando isso for usado (embora no JDK7 você deva ser capaz de suprimi-lo).
Tom Hawtin - tackline
Eu gosto disso também. Você pode fornecer uma amostra de como usar isso com o exemplo dado?
runaros de
@runaros: Usando os comparadores da resposta de KLE: Collections.sort (/ * Collection <Person> * / people, new ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Janus Troelsen de
16

Uma maneira é criar um Comparatorque tenha como argumentos uma lista de propriedades pelas quais classificar, como mostra este exemplo.

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

Ele pode então ser usado em código, por exemplo:

cp = Person.getComparator(Person.SortParameter.ADDRESS_ASCENDING,
                          Person.SortParameter.NAME_DESCENDING);
Collections.sort(personList, cp);
runaros
fonte
Sim. Se você quiser que seu código seja muito genérico, seu enum pode especificar apenas a propriedade a ser lida (você pode usar reflexão para obter a propriedade usando o nome do enum) e pode especificar o resto com um segundo enum: ASC & DESC e possivelmente um terceiro (NULL_FIRST ou NULL_LAST).
KLE
8

Uma abordagem seria compor Comparators. Este poderia ser um método de biblioteca (tenho certeza que existe em algum lugar por aí).

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

Usar:

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

Como alternativa, observe que Collections.sorté uma classificação estável. Se o desempenho não for absolutamente crucial, você deve ser a ordem secundária antes da primária.

Collections.sort(people, addressComparator);
Collections.sort(people, nameComparator);
Tom Hawtin - tackline
fonte
Uma abordagem inteligente, no entanto, poderia ser tornada mais genérica, de modo que inclua um número variável de comparadores, possivelmente incluindo zero?
runaros,
compose(nameComparator, compose(addressComparator, idComparator))Isso seria um pouco melhor se Java tivesse métodos de extensão.
Tom Hawtin - tackline
4

Os comparadores permitem que você faça isso de forma fácil e natural. Você pode criar instâncias únicas de comparadores, seja na própria classe Person ou em uma classe Service associada à sua necessidade.
Exemplos, usando classes 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... 

Se você tiver muitos, também poderá estruturar o código dos comparadores da maneira que desejar. Por exemplo, você poderia:

  • herdar de outro comparador,
  • tem um CompositeComparator que agrega alguns comparadores existentes
  • tem um NullComparator que lida com casos nulos, então delega para outro comparador
  • etc ...
KLE
fonte
2

Acho que acoplar os classificadores à classe Person, como em sua resposta, não é uma boa ideia, porque acopla a comparação (geralmente voltada para os negócios) e o objeto do modelo próximos um do outro. Cada vez que você deseja alterar / adicionar algo ao classificador, você precisa tocar na classe de pessoa, o que geralmente é algo que você não deseja fazer.

Usar um serviço ou algo semelhante, que fornece instâncias do Comparator, como o proposto pelo KLE, parece muito mais flexível e extensível.

gia
fonte
Para mim, isso leva a um acoplamento forte porque de alguma forma a classe titular do comparador TEM que saber a estrutura de dados detalhada de uma classe Person (basicamente quais campos da classe Person comparar) e se você for alterar algo nos campos Person, isso levará ao mesmo mudanças na classe dos comparadores. Acho que os comparadores de Person devem fazer parte de uma classe de Person. blog.sanaulla.info/2008/06/26/…
Stan
2

Minha abordagem é baseada na de Yishai. A principal lacuna é que não há como classificar primeiro ascendente para um atributo e depois descendente para outro. Isso não pode ser feito com enumerações. Para isso usei aulas. Porque SortOrder depende fortemente do tipo, prefiro implementá-lo como uma classe interna de pessoa.

A classe 'Person' com a 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();
    }
}

Um exemplo de uso da classe Person e seu 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
fonte
Qual seria a complexidade desse tipo de encadeamento de comparadores? Estamos essencialmente classificando cada vez que encadeamos os comparadores? Então, fazemos uma operação * log (n) para cada comparador?
John Baum de
0

Recentemente, escrevi um Comparador para classificar vários campos em um registro de String delimitado. Ele permite que você defina o delimitador, a estrutura do registro e as regras de classificação (algumas das quais são específicas do tipo). Você pode usar isso convertendo um registro de pessoa em uma string delimitada.

As informações necessárias são propagadas para o próprio Comparador, seja programaticamente ou por meio de um arquivo XML.

XML é validado por um arquivo XSD embutido no pacote. Por exemplo, abaixo está um layout de registro delimitado por tabulação com quatro campos (dois dos quais são classificáveis):

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

Você então usaria isso em java da seguinte forma:

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

A biblioteca pode ser encontrada aqui:

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

Constantin
fonte
0

Suponha que Coordinatehaja uma classe e seja necessário classificá-la das duas maneiras, de acordo com a coordenada X e a coordenada Y. Para isso, são necessários dois comparadores de diff. Abaixo está o exemplo

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
fonte