Как сравнивать объекты по нескольким полям - PullRequest
190 голосов
/ 15 декабря 2008

Предположим, у вас есть несколько объектов, у которых есть несколько полей, с которыми они могут сравниваться:

public class Person {

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

    /* Constructors */

    /* Methods */

}

Так что в этом примере, когда вы спрашиваете:

a.compareTo(b) > 0

возможно, вы спрашиваете, стоит ли фамилия a перед буквой b, или если a старше b и т. Д. *

Каков самый чистый способ включить множественное сравнение между этими типами объектов без добавления ненужных помех или накладных расходов?

  • java.lang.Comparable интерфейс позволяет сравнивать только по одному полю
  • Добавление многочисленных методов сравнения (т. Е. compareByFirstName(), compareByAge() и т. Д.), На мой взгляд, загромождено.

Так, как лучше всего это сделать?

Ответы [ 21 ]

311 голосов
/ 26 августа 2014

с Java 8:

Comparator.comparing((Person p)->p.firstName)
          .thenComparing(p->p.lastName)
          .thenComparingInt(p->p.age);

Если у вас есть методы доступа:

Comparator.comparing(Person::getFirstName)
          .thenComparing(Person::getLastName)
          .thenComparingInt(Person::getAge);

Если класс реализует Comparable, то такой метод сравнения может использоваться в методе CompareTo:

@Override
public int compareTo(Person o){
    return Comparator.comparing(Person::getFirstName)
              .thenComparing(Person::getLastName)
              .thenComparingInt(Person::getAge)
              .compare(this, o);
}
147 голосов
/ 16 декабря 2008

Вы должны реализовать Comparable <Person>. Предполагая, что все поля не будут нулевыми (для простоты), age - это int, а сравнение ранжирования - first, last, age, метод compareTo довольно прост:

public int compareTo(Person other) {
    int i = firstName.compareTo(other.firstName);
    if (i != 0) return i;

    i = lastName.compareTo(other.lastName);
    if (i != 0) return i;

    return Integer.compare(age, other.age);
}
70 голосов
/ 15 декабря 2008

Вы можете реализовать Comparator, который сравнивает два Person объекта, и вы можете исследовать столько полей, сколько хотите. Вы можете вставить в свой компаратор переменную, которая сообщит ему, с каким полем сравнивать, хотя, вероятно, было бы проще написать несколько компараторов.

58 голосов
/ 20 ноября 2013

(из Дом Кода )

Грязный и запутанный: сортировка вручную

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        int sizeCmp = p1.size.compareTo(p2.size);  
        if (sizeCmp != 0) {  
            return sizeCmp;  
        }  
        int nrOfToppingsCmp = p1.nrOfToppings.compareTo(p2.nrOfToppings);  
        if (nrOfToppingsCmp != 0) {  
            return nrOfToppingsCmp;  
        }  
        return p1.name.compareTo(p2.name);  
    }  
});  

Это требует много печатания, обслуживания и подвержено ошибкам.

Светоотражающий способ: сортировка с помощью BeanComparator

ComparatorChain chain = new ComparatorChain(Arrays.asList(
   new BeanComparator("size"), 
   new BeanComparator("nrOfToppings"), 
   new BeanComparator("name")));

Collections.sort(pizzas, chain);  

Очевидно, что это более кратко, но еще более подвержено ошибкам, так как вы теряете прямую ссылку на поля, используя вместо этого Strings. Теперь, если поле переименовано, компилятор даже не сообщит о проблеме. Более того, поскольку в этом решении используется отражение, сортировка выполняется намного медленнее.

Как добраться: сортировка с помощью Google Guava's ComparisonChain

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return ComparisonChain.start().compare(p1.size, p2.size).compare(p1.nrOfToppings, p2.nrOfToppings).compare(p1.name, p2.name).result();  
        // or in case the fields can be null:  
        /* 
        return ComparisonChain.start() 
           .compare(p1.size, p2.size, Ordering.natural().nullsLast()) 
           .compare(p1.nrOfToppings, p2.nrOfToppings, Ordering.natural().nullsLast()) 
           .compare(p1.name, p2.name, Ordering.natural().nullsLast()) 
           .result(); 
        */  
    }  
});  

Это намного лучше, но требует некоторого кода пластины котла для наиболее распространенного варианта использования: по умолчанию нулевые значения должны оцениваться меньше. Для нулевых полей вы должны предоставить Guava дополнительную директиву, что делать в этом случае. Это гибкий механизм, если вы хотите сделать что-то конкретное, но часто вам нужен регистр по умолчанию (т. Е. 1, a, b, z, null).

Сортировка с помощью Apache Commons CompareToBuilder

Collections.sort(pizzas, new Comparator<Pizza>() {  
    @Override  
    public int compare(Pizza p1, Pizza p2) {  
        return new CompareToBuilder().append(p1.size, p2.size).append(p1.nrOfToppings, p2.nrOfToppings).append(p1.name, p2.name).toComparison();  
    }  
});  

Как и Guar's ComparisonChain, этот библиотечный класс легко сортируется по нескольким полям, но также определяет поведение по умолчанию для нулевых значений (т. Е. 1, a, b, z, null). Однако вы также не можете указать что-либо еще, если не указали свой собственный компаратор.

Таким образом

В конечном итоге все сводится к вкусу и необходимости гибкости (ComparisonChain от Guava) по сравнению с лаконичным кодом (Apache CompareToBuilder).

Бонусный метод

Я нашел хорошее решение, которое объединяет несколько компараторов в порядке приоритета в CodeReview в MultiComparator:

class MultiComparator<T> implements Comparator<T> {
    private final List<Comparator<T>> comparators;

    public MultiComparator(List<Comparator<? super T>> comparators) {
        this.comparators = comparators;
    }

    public MultiComparator(Comparator<? super T>... comparators) {
        this(Arrays.asList(comparators));
    }

    public int compare(T o1, T o2) {
        for (Comparator<T> c : comparators) {
            int result = c.compare(o1, o2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }

    public static <T> void sort(List<T> list, Comparator<? super T>... comparators) {
        Collections.sort(list, new MultiComparator<T>(comparators));
    }
}

Ofcourse Apache Commons Collections уже имеет утилиту для этого:

ComparatorUtils.chainedComparator (comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));
21 голосов
/ 10 июля 2011

@ Patrick Чтобы отсортировать несколько полей подряд, попробуйте ComparatorChain

ComparatorChain - это компаратор, который объединяет один или несколько компараторов в последовательности. ComparatorChain вызывает каждый Comparator последовательно, пока либо 1) какой-либо отдельный Comparator не вернет ненулевой результат (и этот результат затем будет возвращен), либо 2) ComparatorChain исчерпан (и ноль будет возвращен). Этот тип сортировки очень похож на многостолбцовую сортировку в SQL, и этот класс позволяет классам Java эмулировать такое поведение при сортировке списка.

Чтобы еще больше упростить сортировку, подобную SQL, порядок любого отдельного компаратора в списке можно> изменить на обратный.

Вызов метода, который добавляет новые компараторы или изменяет сортировку по возрастанию / убыванию после вызова сравнения (Object, Object), приведет к исключению UnsupportedOperationException. Однако старайтесь не изменять базовый список компараторов или BitSet, который определяет порядок сортировки.

Экземпляры ComparatorChain не синхронизированы. Этот класс не является поточно-ориентированным во время создания, но он обеспечивает многопоточное сравнение после завершения всех операций установки.

19 голосов
/ 01 октября 2014

Другой вариант, который вы всегда можете рассмотреть, - это Apache Commons. Предоставляет множество вариантов.

import org.apache.commons.lang3.builder.CompareToBuilder;

Ex:

public int compare(Person a, Person b){

   return new CompareToBuilder()
     .append(a.getName(), b.getName())
     .append(a.getAddress(), b.getAddress())
     .toComparison();
}
11 голосов
/ 16 декабря 2008

Вы также можете взглянуть на Enum, который реализует Comparator.

http://tobega.blogspot.com/2008/05/beautiful-enums.html

, например

Collections.sort(myChildren, Child.Order.ByAge.descending());
7 голосов
/ 11 ноября 2014

Для тех, кто может использовать API потоковой передачи Java 8, есть более аккуратный подход, который хорошо документирован здесь: Лямбда и сортировка

Я искал эквивалент C # LINQ:

.ThenBy(...)

Я нашел механизм в Java 8 на компараторе:

.thenComparing(...)

Итак, вот фрагмент кода, демонстрирующий алгоритм.

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

Посмотрите приведенную выше ссылку для более точного понимания и объяснения того, как вывод типа Java делает его более неуклюжим по сравнению с LINQ.

Вот полный тестовый блок для справки:

@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);
    }
}
6 голосов
/ 08 июня 2012

Запись Comparator вручную для такого варианта использования - ужасное решение IMO. Такие специальные подходы имеют много недостатков:

  • Нет повторного использования кода. Нарушает СУХОЙ.
  • Boilerplate.
  • Увеличена вероятность ошибок.

Так в чем же решение?

Сначала немного теории.

Обозначим предложение «тип A поддерживает сравнение» через Ord A. (С точки зрения программы вы можете думать о Ord A как об объекте, содержащем логику для сравнения двух A с. Да, точно так же как Comparator.)

Теперь, если Ord A и Ord B, то их составной (A, B) также должен поддерживать сравнение. то есть Ord (A, B). Если Ord A, Ord B и Ord C, то Ord (A, B, C).

Мы можем распространить этот аргумент на произвольную арность и сказать:

Ord A, Ord B, Ord C, ..., Ord ZOrd (A, B, C, .., Z)

Давайте назовем это утверждение 1.

Сравнение композитов будет работать так же, как вы описали в своем вопросе: сначала будет выполнено первое сравнение, затем следующее, затем следующее и т. Д.

Это первая часть нашего решения. Теперь вторая часть.

Если вы знаете, что Ord A, и знаете, как преобразовать B в A (вызвать эту функцию преобразования f), то вы также можете иметь Ord B. Как? Итак, когда нужно сравнить два экземпляра B, вы сначала преобразуете их в A, используя f, а затем применяете Ord A.

Здесь мы отображаем преобразование B → A в Ord A → Ord B. Это называется контравариантным отображением (или comap для краткости).

Ord A, (B → A)comap Ord B

Давайте назовем это утверждение 2.


Теперь давайте применим это к вашему примеру.

У вас есть тип данных с именем Person, который состоит из трех полей типа String.

  • Мы знаем, что Ord String. По утверждению 1 Ord (String, String, String).

  • Мы можем легко написать функцию от Person до (String, String, String). (Просто верните три поля.) Поскольку мы знаем Ord (String, String, String) и Person → (String, String, String), по утверждению 2 мы можем использовать comap, чтобы получить Ord Person.

QED.


Как реализовать все эти концепции?

Хорошая новость в том, что вам не нужно. Уже существует библиотека , которая реализует все идеи, описанные в этом посте. (Если вам интересно, как они реализованы, вы можете заглянуть под капот .)

Вот так будет выглядеть код:

Ord<Person> personOrd = 
 p3Ord(stringOrd, stringOrd, stringOrd).comap(
   new F<Person, P3<String, String, String>>() {
     public P3<String, String, String> f(Person x) {
       return p(x.getFirstName(), x.getLastname(), x.getAge());
     }
   }
 );

Пояснение:

  • stringOrd является объектом типа Ord<String>. Это соответствует нашему первоначальному предложению «поддерживает сравнение».
  • p3Ord - это метод, который принимает Ord<A>, Ord<B>, Ord<C> и возвращает Ord<P3<A, B, C>>. Это соответствует утверждению 1. (P3 означает продукт с тремя элементами. Продукт является алгебраическим термином для композитов.)
  • comap соответствует скважине, comap.
  • F<A, B> представляет функцию преобразования A → B.
  • p - это фабричный метод создания продуктов.
  • Полное выражение соответствует утверждению 2.

Надеюсь, это поможет.

6 голосов
/ 26 июня 2013
import com.google.common.collect.ComparisonChain;

/**
 * @author radler
 * Class Description ...
 */
public class Attribute implements Comparable<Attribute> {

    private String type;
    private String value;

    public String getType() { return type; }
    public void setType(String type) { this.type = type; }

    public String getValue() { return value; }
    public void setValue(String value) { this.value = value; }

    @Override
    public String toString() {
        return "Attribute [type=" + type + ", value=" + value + "]";
    }

    @Override
    public int compareTo(Attribute that) {
        return ComparisonChain.start()
            .compare(this.type, that.type)
            .compare(this.value, that.value)
            .result();
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...