Jak sortować według dwóch pól w Javie?

171

Mam tablicę obiektów person (int age; String name;).

Jak mogę posortować tę tablicę alfabetycznie według nazwy, a następnie według wieku?

Którego algorytmu użyłbyś do tego?

Damir
źródło

Odpowiedzi:

221

Możesz użyć Collections.sortw następujący sposób:

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> jest teraz posortowane według nazwy, a następnie według wieku.

String.compareTo„Porównuje leksykograficznie dwa ciągi znaków” - z dokumentacji .

Collections.sortjest metodą statyczną w natywnej bibliotece kolekcji. Dokonuje rzeczywistego sortowania, wystarczy dostarczyć komparator, który definiuje sposób porównywania dwóch elementów na liście: osiąga się to poprzez dostarczenie własnej implementacji comparemetody.

Richard H.
źródło
10
Możesz również dodać parametr typu, Comparatoraby uniknąć konieczności rzutowania danych wejściowych.
biziclop
@Ralph: Poprawiłem odpowiedź i dodałem krótki opis.
Richard H
Ponieważ OP ma już własną klasę obiektów, bardziej sensowne byłoby wdrożenie Comparable. Zobacz odpowiedź: @ berry120
Zulaxia
1
Mini przegląd kodu: klauzula else jest zbędna, ponieważ pierwszy powrót działa jako klauzula ochronna. Świetna odpowiedź była dla mnie ucztą.
Tom Saleeba,
30
Ponieważ to pytanie / odpowiedź wciąż jest łączone, należy zauważyć, że w przypadku Java SE 8 stało się to znacznie prostsze. Jeśli są gettery, możesz napisaćComparator<Person> comparator = Comparator.comparing(Person::getName).thenComparingInt(Person::getAge);
Puce
143

Dla tych, którzy potrafią korzystać z API strumieniowego Java 8, istnieje lepsze podejście, które jest dobrze udokumentowane tutaj: Lambdy i sortowanie

Szukałem odpowiednika C # LINQ:

.ThenBy(...)

Znalazłem mechanizm w Javie 8 w Komparatorze:

.thenComparing(...)

Oto fragment, który demonstruje algorytm.

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

Sprawdź powyższy link, aby uzyskać ładniejszy sposób i wyjaśnienie, w jaki sposób wnioskowanie o typie w Javie sprawia, że ​​definiowanie jest nieco bardziej niezgrabne w porównaniu z LINQ.

Oto pełny test jednostkowy w celach informacyjnych:

@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);
    }
}
Łukasza Machowskiego
źródło
Jaka byłaby złożoność tego rodzaju łączenia w łańcuchy komparatorów? Czy w zasadzie sortujemy za każdym razem, gdy łączymy w łańcuch komparatorów? Czyli wykonujemy operację NlogN dla każdego komparatora?
John Baum
17
Jeśli są gettery, możesz napisaćComparator<Person> comparator = Comparator.comparing(Person::getName).thenComparing(Person::getAge);
Puce
1
Użyj thenComparingIntdla wieku (int)
Puce
Składnia z labda '->' nie działa dla mnie. Osoba :: getLastName tak.
Noldy
Po utworzeniu komparatora, jaka jest potrzeba stworzenia strumienia, posortowania go z komparatorem, a następnie zebrania? Czy możesz po prostu użyć Collections.sort(people, comparator);zamiast tego?
Anish Sana,
107

Korzystanie z podejścia strumieniowego Java 8 ...

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

A podejście 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());
        } 
    });

W końcu...

//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.
źródło
19

Musisz zaimplementować własny Comparator, a następnie z niego skorzystać: na przykład

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

Twój komparator może wyglądać trochę tak:

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

Komparator najpierw porównuje nazwiska, jeśli nie są one równe, zwraca wynik z porównania, w przeciwnym razie zwraca wynik porównania, porównując wiek obu osób.

Ten kod jest tylko szkicem: ponieważ klasa jest niezmienna, możesz pomyśleć o zbudowaniu jej singletona, zamiast tworzyć nową instancję dla każdego sortowania.

Ralph
źródło
16

Aby to osiągnąć, możesz użyć podejścia Java 8 Lambda. Lubię to:

persons.sort(Comparator.comparing(Person::getName).thenComparing(Person::getAge));
Zia Ul Mustafa
źródło
15

Zaimplementuj klasę osoby, Comparable<Person>a następnie zaimplementuj metodę compareTo, na przykład:

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

To posortuje najpierw według nazwy (bez rozróżniania wielkości liter), a następnie według wieku. Następnie możesz uruchomić Arrays.sort()lub Collections.sort()na kolekcji lub tablicy obiektów Person.

Michael Berry
źródło
Generalnie wolę to od tworzenia komparatora, ponieważ, jak mówi berry120, możesz sortować za pomocą wbudowanych metod, zamiast zawsze używać swojego niestandardowego komparatora.
Zulaxia
4

Guawa ComparisonChainzapewnia czysty sposób na zrobienie tego. Skorzystaj z tego łącza .

Narzędzie do wykonywania łańcuchowej instrukcji porównawczej. Na przykład:

   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
źródło
4

Możesz zrobić tak:

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
źródło
3

Zastosowanie Comparator, a następnie umieścić przedmiotów do Collection, a następnieCollections.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
źródło
3

Utwórz tyle komparatorów, ile potrzeba. Następnie wywołaj metodę „thenComparing” dla każdej kategorii zamówienia. To sposób na zrobienie przez strumienie. Widzieć:

//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));        

Wygląd: sortowanie obiektu zdefiniowanego przez użytkownika w wielu polach - komparator (strumień lambda)

Fabian Brandão
źródło
3

Byłbym ostrożny przy używaniu guawy, ComparisonChainponieważ tworzy jej instancję dla każdego elementu, więc patrzyłbyś na tworzenie N x Log Nłańcuchów porównawczych tylko po to, aby porównać, jeśli sortujesz, lubN instancje, jeśli iterujesz i sprawdzasz pod kątem równości.

Zamiast tego utworzyłbym statyczny Comparatorprzy użyciu najnowszego API Java 8, jeśli to możliwe lub OrderingAPI Guavy, które pozwala to zrobić, oto przykład z Javą 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);
}

Oto jak korzystać z OrderingAPI Guavy : https://github.com/google/guava/wiki/OrderingExplained

Guido Medina
źródło
1
„… tworzy jego instancję na każdy porównywany element…” - to nieprawda. Przynajmniej w nowoczesnych wersjach guawy, wzywających do comparemetody nie tworzy niczego, ale zwraca jedną z pojedynczych przypadkach LESS, GREATERlub ACTIVEw zależności od wyniku porównania. Jest to wysoce zoptymalizowane podejście i nie powoduje zwiększenia ilości pamięci ani narzutu wydajności.
Yoory N.
Tak; Właśnie spojrzałem na kod źródłowy, rozumiem, co masz na myśli, ale ze względu na zależności byłbym bardziej skłonny do korzystania z nowego interfejsu API porównującego Java 8.
Guido Medina
2

Lub możesz wykorzystać fakt, że Collections.sort()(lub Arrays.sort()) jest stabilny (nie zmienia kolejności elementów, które są równe) i użyć Comparatornajpierw do sortowania według wieku, a następnie innego do sortowania według nazwy.

W tym konkretnym przypadku nie jest to dobry pomysł, ale jeśli musisz mieć możliwość zmiany kolejności sortowania w czasie wykonywania, może to być przydatne.

biziclop
źródło
2

Możesz użyć ogólnego Komparatora szeregowego do sortowania kolekcji według wielu pól.

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
źródło
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;
    }

}

Zaktualizowana wersja:

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
źródło
Obsługa wyjątków nullpointer jest przyjemna i wyjaśnia, że ​​nie będzie działać z wartością null, ale i tak zostanie podniesiona
Ralph
Masz absolutną rację. Ostatnio sprawdzałem niektóre wartości do skopiowania z jednego miejsca do drugiego, a teraz robię to wszędzie.
fmucar
0

W przypadku takiej klasy Book:

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

sortowanie klasy głównej za pomocą pozorowanych obiektów

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
źródło
0

Nie jestem pewien, czy pisanie compartor wewnątrz klasy Person jest w tym przypadku brzydkie. Czy to tak:

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
źródło