Como classificar por dois campos em Java?

171

Eu tenho matriz de objetos person (int age; String name;).

Como posso classificar essa matriz em ordem alfabética por nome e depois por idade?

Qual algoritmo você usaria para isso?

Damir
fonte

Respostas:

221

Você pode usar da Collections.sortseguinte maneira:

private static void order(List<Person> persons) {

    Collections.sort(persons, new Comparator() {

        public int compare(Object o1, Object o2) {

            String x1 = ((Person) o1).getName();
            String x2 = ((Person) o2).getName();
            int sComp = x1.compareTo(x2);

            if (sComp != 0) {
               return sComp;
            } 

            Integer x1 = ((Person) o1).getAge();
            Integer x2 = ((Person) o2).getAge();
            return x1.compareTo(x2);
    }});
}

List<Persons> agora é classificado por nome e depois por idade.

String.compareTo"Compara duas strings lexicograficamente" - dos documentos .

Collections.sorté um método estático na biblioteca de coleções nativa. Ele faz a classificação real, você só precisa fornecer um Comparador que define como dois elementos da sua lista devem ser comparados: isso é conseguido fornecendo sua própria implementação do comparemétodo.

Richard H
fonte
10
Você também pode adicionar um parâmetro de tipo Comparatorpara evitar a necessidade de converter as entradas.
precisa saber é
@ Ralph: Eu alterei minha resposta e adicionei uma breve descrição.
Richard H
Como o OP já possui sua própria classe de objeto, faria mais sentido implementá-lo Comparable. Veja a resposta por @ berry120
Zulaxia 31/03
1
Revisão de minicódigo: a cláusula else é redundante porque o primeiro retorno atua como uma cláusula de guarda. Ótima resposta, porém, funcionou para mim.
precisa saber é o seguinte
30
Como essa pergunta / resposta ainda é vinculada, observe que, com o Java SE 8, isso se tornou muito mais simples. Se houver getters você pode escreverComparator<Person> comparator = Comparator.comparing(Person::getName).thenComparingInt(Person::getAge);
Puce
143

Para aqueles capazes de usar a API de streaming Java 8, existe uma abordagem mais limpa que está bem documentada aqui: Lambdas e classificação

Eu estava procurando o equivalente do C # LINQ:

.ThenBy(...)

Encontrei o mecanismo no Java 8 no comparador:

.thenComparing(...)

Então, aqui está o trecho que demonstra o algoritmo.

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

Confira o link acima para obter uma maneira mais clara e uma explicação sobre como a inferência de tipo do Java torna um pouco mais difícil de definir em comparação com o LINQ.

Aqui está o teste de unidade completo para referência:

@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);
    }
}
Luke Machowski
fonte
Qual seria a complexidade desse tipo de encadeamento de comparadores? Essencialmente, estamos classificando cada vez que encadeamos os comparadores? Então, fazemos uma operação NlogN para cada comparador?
John Baum
17
Se houver getters, você pode escrever #Comparator<Person> comparator = Comparator.comparing(Person::getName).thenComparing(Person::getAge);
Puce
1
Use thenComparingIntfor age (int)
Puce
Sintaxe com o labda '->' não funciona para mim. A pessoa :: getLastName faz.
Noldy 30/03
Após criar o comparador, qual é a necessidade de criar um fluxo, classificá-lo com o comparador e coletá-lo? Você pode apenas usar Collections.sort(people, comparator);?
Anish Sana
107

Usando a abordagem Java 8 Streams ...

//Creates and sorts a stream (does not sort the original list)       
persons.stream().sorted(Comparator.comparing(Person::getName).thenComparing(Person::getAge));

E a abordagem Java 8 Lambda ...

//Sorts the original list Lambda style
persons.sort((p1, p2) -> {
        if (p1.getName().compareTo(p2.getName()) == 0) {
            return p1.getAge().compareTo(p2.getAge());
        } else {
            return p1.getName().compareTo(p2.getName());
        } 
    });

Por fim ...

//This is similar SYNTAX to the Streams above, but it sorts the original list!!
persons.sort(Comparator.comparing(Person::getName).thenComparing(Person::getAge));
Bradley D
fonte
19

Você precisa implementar o seu próprio Comparatore usá-lo: por exemplo

Arrays.sort(persons, new PersonComparator());

O seu comparador pode ficar assim:

public class PersonComparator implements Comparator<? extends Person> {

  public int compare(Person p1, Person p2) {
     int nameCompare = p1.name.compareToIgnoreCase(p2.name);
     if (nameCompare != 0) {
        return nameCompare;
     } else {
       return Integer.valueOf(p1.age).compareTo(Integer.valueOf(p2.age));
     }
  }
}

O comparador primeiro compara os nomes; se eles não são iguais, retorna o resultado da comparação; caso contrário, retorna o resultado da comparação ao comparar as idades de ambas as pessoas.

Esse código é apenas um rascunho: como a classe é imutável, você pode pensar em criar um único nome, em vez de criar uma nova instância para cada classificação.

Ralph
fonte
16

Você pode usar a abordagem Java 8 Lambda para conseguir isso. Como isso:

persons.sort(Comparator.comparing(Person::getName).thenComparing(Person::getAge));
Zia Ul Mustafa
fonte
15

Faça sua classe pessoal implementar Comparable<Person>e, em seguida, implemente o método compareTo, por exemplo:

public int compareTo(Person o) {
    int result = name.compareToIgnoreCase(o.name);
    if(result==0) {
        return Integer.valueOf(age).compareTo(o.age);
    }
    else {
        return result;
    }
}

Isso será classificado primeiro pelo nome (sem distinção entre maiúsculas e minúsculas) e depois por idade. Em seguida, você pode executar Arrays.sort()ou Collections.sort()na coleção ou matriz de objetos Pessoa.

Michael Berry
fonte
Geralmente, prefiro isso do que fazer um comparador, pois, como diz o berry120, você pode classificar com métodos internos, em vez de precisar sempre usar seu comparador personalizado.
Zulaxia 31/03
4

Goiaba ComparisonChainfornece uma maneira limpa de fazê-lo. Consulte este link .

Um utilitário para executar uma instrução de comparação encadeada. Por exemplo:

   public int compareTo(Foo that) {
     return ComparisonChain.start()
         .compare(this.aString, that.aString)
         .compare(this.anInt, that.anInt)
         .compare(this.anEnum, that.anEnum, Ordering.natural().nullsLast())
         .result();
   }
Pritesh Mhatre
fonte
4

Você pode fazer assim:

List<User> users = Lists.newArrayList(
  new User("Pedro", 12), 
  new User("Maria", 10), 
  new User("Rafael",12)
);

users.sort(
  Comparator.comparing(User::getName).thenComparing(User::getAge)
);
rafambbr
fonte
3

Use Comparatore em seguida, colocar objetos em Collection, em seguida,Collections.sort();

class Person {

    String fname;
    String lname;
    int age;

    public Person() {
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getFname() {
        return fname;
    }

    public void setFname(String fname) {
        this.fname = fname;
    }

    public String getLname() {
        return lname;
    }

    public void setLname(String lname) {
        this.lname = lname;
    }

    public Person(String fname, String lname, int age) {
        this.fname = fname;
        this.lname = lname;
        this.age = age;
    }

    @Override
    public String toString() {
        return fname + "," + lname + "," + age;
    }
}

public class Main{

    public static void main(String[] args) {
        List<Person> persons = new java.util.ArrayList<Person>();
        persons.add(new Person("abc3", "def3", 10));
        persons.add(new Person("abc2", "def2", 32));
        persons.add(new Person("abc1", "def1", 65));
        persons.add(new Person("abc4", "def4", 10));
        System.out.println(persons);
        Collections.sort(persons, new Comparator<Person>() {

            @Override
            public int compare(Person t, Person t1) {
                return t.getAge() - t1.getAge();
            }
        });
        System.out.println(persons);

    }
}
Jigar Joshi
fonte
3

Crie quantos comparadores forem necessários. Depois, chame o método "thenComparing" para cada categoria de pedido. É uma maneira de fazer pelo Streams. Vejo:

//Sort by first and last name
System.out.println("\n2.Sort list of person objects by firstName then "
                                        + "by lastName then by age");
Comparator<Person> sortByFirstName 
                            = (p, o) -> p.firstName.compareToIgnoreCase(o.firstName);
Comparator<Person> sortByLastName 
                            = (p, o) -> p.lastName.compareToIgnoreCase(o.lastName);
Comparator<Person> sortByAge 
                            = (p, o) -> Integer.compare(p.age,o.age);

//Sort by first Name then Sort by last name then sort by age
personList.stream().sorted(
    sortByFirstName
        .thenComparing(sortByLastName)
        .thenComparing(sortByAge)
     ).forEach(person->
        System.out.println(person));        

Look: classifique o objeto definido pelo usuário em vários campos - Comparador (fluxo lambda)

Fabian Brandão
fonte
3

Eu tomaria cuidado ao usar o Guava's, ComparisonChainpois ele cria uma instância por elemento que foi comparada, para que você analise a criação de N x Log Ncadeias de comparação apenas para comparar se estiver classificando ouN instâncias se estiver repetindo e verificando a igualdade.

Em vez disso, eu criaria uma estática Comparatorusando a API Java 8 mais recente, se possível, ou a OrderingAPI do Guava, que permite fazer isso, aqui está um exemplo com o Java 8:

import java.util.Comparator;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsLast;

private static final Comparator<Person> COMPARATOR = Comparator
  .comparing(Person::getName, nullsLast(naturalOrder()))
  .thenComparingInt(Person::getAge);

@Override
public int compareTo(@NotNull Person other) {
  return COMPARATOR.compare(this, other);
}

Aqui está como usar a OrderingAPI do Guava : https://github.com/google/guava/wiki/OrderingExplained

Guido Medina
fonte
1
"... cria uma instância dele por elemento que foi comparado ..." - isso não é verdade. Pelo menos nas versões modernas do Guava, chamar o comparemétodo não cria nada, mas retorna uma das instâncias singleton LESS, GREATERou ACTIVEdependendo do resultado da comparação. Essa é uma abordagem altamente otimizada e não adiciona sobrecarga de memória ou desempenho.
Yoory N.
Sim; Acabei de olhar o código-fonte agora, entendo o que você quer dizer, mas estaria mais inclinado a usar a nova API de comparação do Java 8 para o bem das dependências.
Guido Medina
2

Ou você pode explorar o fato de que Collections.sort()(ou Arrays.sort()) é estável (não reordena elementos iguais) e usa umComparator para classificar por idade primeiro e depois outro para classificar por nome.

Nesse caso específico, essa não é uma ideia muito boa, mas se você precisar alterar a ordem de classificação em tempo de execução, pode ser útil.

Biziclop
fonte
2

Você pode usar o comparador serial genérico para classificar coleções por vários campos.

import org.apache.commons.lang3.reflect.FieldUtils;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
* @author MaheshRPM
*/
public class SerialComparator<T> implements Comparator<T> {
List<String> sortingFields;

public SerialComparator(List<String> sortingFields) {
    this.sortingFields = sortingFields;
}

public SerialComparator(String... sortingFields) {
    this.sortingFields = Arrays.asList(sortingFields);
}

@Override
public int compare(T o1, T o2) {
    int result = 0;
    try {
        for (String sortingField : sortingFields) {
            if (result == 0) {
                Object value1 = FieldUtils.readField(o1, sortingField, true);
                Object value2 = FieldUtils.readField(o2, sortingField, true);
                if (value1 instanceof Comparable && value2 instanceof Comparable) {
                    Comparable comparable1 = (Comparable) value1;
                    Comparable comparable2 = (Comparable) value2;
                    result = comparable1.compareTo(comparable2);
                } else {
                    throw new RuntimeException("Cannot compare non Comparable fields. " + value1.getClass()
                            .getName() + " must implement Comparable<" + value1.getClass().getName() + ">");
                }
            } else {
                break;
            }
        }
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
    return result;
}
}
Maheshkumar
fonte
0
Arrays.sort(persons, new PersonComparator());



import java.util.Comparator;

public class PersonComparator implements Comparator<? extends Person> {

    @Override
    public int compare(Person o1, Person o2) {
        if(null == o1 || null == o2  || null == o1.getName() || null== o2.getName() ){
            throw new NullPointerException();
        }else{
            int nameComparisonResult = o1.getName().compareTo(o2.getName());
            if(0 == nameComparisonResult){
                return o1.getAge()-o2.getAge();
            }else{
                return nameComparisonResult;
            }
        }
    }
}


class Person{
    int age; String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

Versão atualizada:

public class PersonComparator implements Comparator<? extends Person> {

   @Override
   public int compare(Person o1, Person o2) {

      int nameComparisonResult = o1.getName().compareToIgnoreCase(o2.getName());
      return 0 == nameComparisonResult?o1.getAge()-o2.getAge():nameComparisonResult;

   }
 }
fmucar
fonte
O tratamento de exceção nullpointer é agradável e deixar claro que ele não iria funcionar com nulo, mas seria levantado qualquer maneira
Ralph
Você está absolutamente certo. Recentemente, eu costumava verificar alguns valores para copiar de um lugar para outro e agora continuo fazendo isso em todos os lugares.
Fmucar
0

Para uma classe Bookcomo esta:

package books;

public class Book {

    private Integer id;
    private Integer number;
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "book{" +
                "id=" + id +
                ", number=" + number +
                ", name='" + name + '\'' + '\n' +
                '}';
    }
}

classificando a classe principal com objetos simulados

package books;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;


public class Main {

    public static void main(String[] args) {
        System.out.println("Hello World!");

        Book b = new Book();

        Book c = new Book();

        Book d = new Book();

        Book e = new Book();

        Book f = new Book();

        Book g = new Book();
        Book g1 = new Book();
        Book g2 = new Book();
        Book g3 = new Book();
        Book g4 = new Book();




        b.setId(1);
        b.setNumber(12);
        b.setName("gk");

        c.setId(2);
        c.setNumber(12);
        c.setName("gk");

        d.setId(2);
        d.setNumber(13);
        d.setName("maths");

        e.setId(3);
        e.setNumber(3);
        e.setName("geometry");

        f.setId(3);
        f.setNumber(34);
        b.setName("gk");

        g.setId(3);
        g.setNumber(11);
        g.setName("gk");

        g1.setId(3);
        g1.setNumber(88);
        g1.setName("gk");
        g2.setId(3);
        g2.setNumber(91);
        g2.setName("gk");
        g3.setId(3);
        g3.setNumber(101);
        g3.setName("gk");
        g4.setId(3);
        g4.setNumber(4);
        g4.setName("gk");





        List<Book> allBooks = new ArrayList<Book>();

        allBooks.add(b);
        allBooks.add(c);
        allBooks.add(d);
        allBooks.add(e);
        allBooks.add(f);
        allBooks.add(g);
        allBooks.add(g1);
        allBooks.add(g2);
        allBooks.add(g3);
        allBooks.add(g4);



        System.out.println(allBooks.size());


        Collections.sort(allBooks, new Comparator<Book>() {

            @Override
            public int compare(Book t, Book t1) {
                int a =  t.getId()- t1.getId();

                if(a == 0){
                    int a1 = t.getNumber() - t1.getNumber();
                    return a1;
                }
                else
                    return a;
            }
        });
        System.out.println(allBooks);

    }


   }
Jackey Dada Bob
fonte
0

Não tenho certeza se é feio escrever o compartimento dentro da classe Person neste caso. Fez assim:

public class Person implements Comparable <Person> {

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

    public Person(String firstName, String lastName, int BirthDay) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = BirthDay;
    }

    public int getAge() {
        return age;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    @Override
    public int compareTo(Person o) {
        // default compareTo
    }

    @Override
    public String toString() {
        return firstName + " " + lastName + " " + age + "";
    }

    public static class firstNameComperator implements Comparator<Person> {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.firstName.compareTo(o2.firstName);
        }
    }

    public static class lastNameComperator implements Comparator<Person> {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.lastName.compareTo(o2.lastName);
        }
    }

    public static class ageComperator implements Comparator<Person> {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.age - o2.age;
        }
    }
}
public class Test {
    private static void print() {
       ArrayList<Person> list = new ArrayList();
        list.add(new Person("Diana", "Agron", 31));
        list.add(new Person("Kay", "Panabaker", 27));
        list.add(new Person("Lucy", "Hale", 28));
        list.add(new Person("Ashley", "Benson", 28));
        list.add(new Person("Megan", "Park", 31));
        list.add(new Person("Lucas", "Till", 27));
        list.add(new Person("Nicholas", "Hoult", 28));
        list.add(new Person("Aly", "Michalka", 28));
        list.add(new Person("Adam", "Brody", 38));
        list.add(new Person("Chris", "Pine", 37));
        Collections.sort(list, new Person.lastNameComperator());
        Iterator<Person> it = list.iterator();
        while(it.hasNext()) 
            System.out.println(it.next().toString()); 
     }  
}    
Tiago Redaelli
fonte