Як сортувати список за різними параметрами за різний час


95

У мене є клас з іменем Personіз декількома властивостями, наприклад:

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

Багато об'єктів Personзберігаються в ArrayList<Person>. Я хочу відсортувати цей список за кількома параметрами сортування і час від часу різними. Наприклад, я можу одного разу захотіти сортувати за nameзростанням, а потім за addressспаданням, а інший раз просто за idспаданням.

І я не хочу створювати власні методи сортування (тобто я хочу використовувати Collections.sort(personList, someComparator). Яке найелегантніше рішення, яким цього вдається досягти?

Відповіді:


193

Я думаю, що ваш підхід до перерахування в основному обґрунтований, але оператори перемикання дійсно потребують більш об'єктно-орієнтованого підходу. Поміркуйте:

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

Приклад використання (зі статичним імпортом).

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

12
+1 розумне використання перелічень. Мені подобається елегантне поєднання, яке ви робите з переліками, "низхідним" та "складеним". Я думаю, лікування нульових значень відсутнє, але його легко додати так само, як "за спаданням".
KLE

1
Багато приємних відповідей, що дають підставу для роздумів. Оскільки жодна відповідь не виділялася як чітка альтернатива, я прийму цю, тому що мені подобається елегантність, але я закликаю тих, хто переглядає цю відповідь, перевірити й інші підходи.
runaros

1
@TheLittleNaruto, метод порівняння повертає від'ємне число, якщо о2 більше, позитивне, якщо о1 більше, і нуль, якщо вони рівні. Помноження на -1 змінює результат, тобто ідею обведення (протилежне звичному порядку зростання), залишаючи його рівним нулю, якщо вони були рівними.
Yishai

5
Зверніть увагу, що з Java 8 ви можете використовувати comparator.reversed()для спадання та можна використовувати comparator1.thenComparing(comparator2)для ланцюжкового порівняння.
GuiSim

1
@JohnBaum, якщо перший компаратор повертає ненульовий результат, цей результат повертається, а решта ланцюжка не виконується.
Yishai

26

Ви можете створити компаратори для кожного з властивостей, які ви можете відсортувати, а потім спробувати "ланцюжок порівняння" :-) так:

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

Ви, ймовірно, отримаєте попередження, коли воно буде використано (хоча в JDK7 ви повинні мати можливість його придушити).
Tom Hawtin - таклін

Мені це теж подобається. Чи можете ви навести зразок того, як це використовувати з наведеним прикладом?
runaros

@runaros: Використання порівняльників з відповіді KLE: Collections.sort (/ * Collection <Person> * / people, new ChainedComparator (NAME_ASC_ADRESS_DESC, ID_DESC));
Янус Троельсен,

16

Один із способів - створити такий, Comparatorякий приймає в якості аргументів список властивостей, за якими можна сортувати, як показано в цьому прикладі.

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

Потім його можна використовувати в коді, наприклад, таким чином:

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

Так. Якщо ви хочете, щоб ваш код був дуже загальним, ваш перелік міг вказати лише властивість для читання (ви можете використовувати відображення, щоб отримати властивість за допомогою імені перерахування), а решту можна вказати за допомогою другого перерахування: ASC & DESC, і можливо, третя (NULL_FIRST або NULL_LAST).
KLE

8

Одним із підходів було б складання Comparators. Це може бути бібліотечний метод (я впевнений, що він існує десь там).

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

Використання:

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

Крім того, зверніть увагу, що Collections.sortце стабільний сорт. Якщо ефективність не є абсолютно важливою, ви сортуєте як вторинний порядок перед основним.

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

Однак розумний підхід, чи можна зробити його більш загальним, таким, що він включає змінну кількість компараторів, можливо, включаючи нуль?
runaros

compose(nameComparator, compose(addressComparator, idComparator))Це читалося б трохи краще, якби Java мала методи розширення.
Том Хоутін - таклін

4

Порівняльники дозволяють робити це дуже легко і природно. Ви можете створювати одиничні екземпляри компараторів як у самому класі Person, так і в класі Service, пов’язаному з вашими потребами.
Приклади використання анонімних внутрішніх класів:

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

Якщо їх багато, ви також можете структурувати свій код порівняння будь-яким способом. Наприклад, ви можете:

  • успадкувати від іншого компаратора,
  • мати CompositeComparator, який об’єднує деякі існуючі компаратори
  • мати NullComparator, який обробляє нульові випадки, а потім делегує інший компаратор
  • тощо ...

2

Я думаю, що пов’язувати сортувальники з класом Person, як у вашій відповіді, не є гарною ідеєю, оскільки воно поєднує порівняння (як правило, ділове) та об’єкт моделі, щоб наблизитись один до одного. Кожного разу, коли ви хочете змінити / додати щось сортувальник, вам потрібно торкнутися класу людини, що, як правило, ви не хочете робити.

Використання Служби або чогось подібного, що надає екземпляри компаратора, як пропонується KLE, звучить набагато гнучкіше та розширюваніше.


Як на мене, це призводить до щільного зчеплення, оскільки якось клас власника порівняльників МАЄ знати детальну структуру даних класу Person (в основному, які поля класу Person порівнювати), і якщо ви коли-небудь щось зміните в полях Persons, це призведе до того, що зміни в класі компараторів. Я вважаю, що компаратори Person повинні бути частиною класу Person. blog.sanaulla.info/2008/06/26/…
Стен

2

Мій підхід спирається на підхід Ішая. Основна прогалина полягає в тому, що немає можливості сортувати спочатку за зростанням за атрибутом, а після цього за обрисом для іншого. Цього не можна зробити за допомогою переліків. Для цього я використовував заняття. Оскільки SortOrder сильно залежить від типу, який я вважав за краще застосовувати як внутрішній клас людини.

Клас "Person" із внутрішнім класом "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();
    }
}

Приклад використання класу Person та його 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


У чому полягає складність такого роду ланцюжків порівнянь? Чи ми, по суті, сортуємо кожного разу, коли об’єднуємо компаратори? Отже, ми робимо операцію * log (n) для кожного компаратора?
Джон Баум,

0

Нещодавно я написав компаратор для сортування кількох полів у розділеному записі рядка. Це дозволяє визначити роздільник, структуру записів та правила сортування (деякі з них є типовими). Ви можете використати це, перетворивши запис Person у рядок із роздільниками.

Необхідна інформація передається до самого компаратора або програмно, або через файл XML.

XML перевіряється вбудованим у пакет файлом XSD. Наприклад, нижче наведено макет запису, розділений табуляцією, з чотирма полями (два з яких можна сортувати):

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

Потім ви б використовували це в Java так:

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

Бібліотеку можна знайти тут:

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


0

Припустимо, що існує клас Coordinate, і його потрібно сортувати обома способами за координатою X та координатою Y. Для цього потрібні два компаратори Diffnetnet. Нижче наведено зразок

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
        };
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.