Как отсортировать список по разным параметрам в разное время

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 в основном здравый, но операторы switch действительно нуждаются в более объектно-ориентированном подходе. Рассматривать:

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, метод сравнения возвращает отрицательное число, если o2 больше, положительное, если o1 больше, и ноль, если они равны. Умножение на -1 меняет результат, который является идеей убывания (противоположный обычному порядку возрастания), оставляя его равным нулю, если они были равны.
Ишай
5
Обратите внимание, что начиная с Java 8 вы можете использовать comparator.reversed()нисходящие comparator1.thenComparing(comparator2)компараторы и связывать их.
GuiSim
1
@JohnBaum, если первый компаратор возвращает ненулевой результат, этот результат возвращается, а остальная часть цепочки не выполняется.
Ишай
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 вы должны иметь возможность подавить его).
Том Хотин - tackline
Мне это тоже нравится. Можете ли вы предоставить образец того, как использовать это в данном примере?
runaros
@runaros: Использование компараторов из ответа KLE: Collections.sort (/ * Коллекция <Person> * / people, новый 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);
Том Хотин - tackline
источник
Однако можно ли сделать умный подход более общим, чтобы он включал переменное количество компараторов, возможно, включая ноль?
runaros
compose(nameComparator, compose(addressComparator, idComparator))Было бы лучше, если бы в Java были методы расширения.
Том Хотин - tackline
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, который обрабатывает нулевые случаи, а затем делегирует другому компаратору
  • и т.д...
KLE
источник
2

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

Использование службы или чего-то подобного, которое предоставляет экземпляры Comparator, такие как предложенный KLE, звучит более гибко и расширяемо.

Джиа
источник
Что касается меня, это приводит к тесной связи, потому что каким-то образом класс держателя компараторов ДОЛЖЕН знать подробную структуру данных класса Person (в основном, какие поля класса Person для сравнения), и если вы когда-нибудь собираетесь что-то изменить в полях Persons, это приведет к тому же изменения в классе компараторов. Я предполагаю, что компараторы Person должны быть частью класса Person. blog.sanaulla.info/2008/06/26/…
Stan
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

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. Для этого необходимы два разных компаратора. Ниже образец

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
        };
    }
}
рави ранджан
источник