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

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

public class Person {

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

    /* Constructors */

    /* Methods */

}

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

a.compareTo(b) > 0

вы можете спросить, будет ли последнее имя до b, или если a старше b и т.д.

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

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

Итак, каков наилучший способ сделать это?

Ответ 1

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

Ответ 2

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

Ответ 3

Вы должны реализовать Comparable <Person>. Предполагая, что все поля не будут пустыми (для простоты), этот возраст является int, а ранжирование сравнения - это первый, последний, возраст, метод 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);
}

Ответ 4

(из Дом кода)

Беспокойный и запутанный: Сортировка вручную

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

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

Как добраться: Сортировка с Google Guavas 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();  
    }  
});  

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

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

В конечном счете это сводится к вкусу и необходимости гибкости (Guavas ComparisonChain) по сравнению с кратким кодом (Apaches 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 уже используют для этого:

ComparatorUtils.chainedComparator(comparatorCollection)

Collections.sort(list, ComparatorUtils.chainedComparator(comparators));

Ответ 5

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

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

Для дальнейшего облегчения сортировки, подобной SQL, порядок любого отдельного компаратора в списке может быть отменен.

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

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

Ответ 6

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

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

Пример:

public int compare(Person a, Person b){

   return new CompareToBuilder()
     .append(a.getName(), b.getName())
     .append(a.getAddress(), b.getAddress())
     .toComparison();
}

Ответ 8

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

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

.ThenBy(...)

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

.thenComparing(...)

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

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

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

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

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

Ответ 9

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

  • Повторное использование кода отсутствует. Нарушает DRY.
  • Boilerplate.
  • Повышенная вероятность ошибок.

Итак, какое решение?

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

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

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

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

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) comapOrd 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 - это метод factory для создания продуктов.
  • Все выражение соответствует утверждению 2.

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

Ответ 10

Вместо методов сравнения вы можете просто определить несколько типов подклассов "Компаратор" внутри класса Person. Таким образом вы можете передать их в стандартные методы сортировки коллекций.

Ответ 11

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

}

Ответ 12

Я думаю, что было бы более запутанным, если бы ваш алгоритм сравнения был "умным". Я бы пошел с многочисленными методами сравнения, которые вы предложили.

Единственным исключением для меня было бы равенство. Для модульного тестирования мне было полезно переопределить .Equals(в .net), чтобы определить, равно ли несколько полей между двумя объектами (а не то, что ссылки равны).

Ответ 13

Если есть несколько способов, которыми пользователь может заказать человека, вы также можете установить несколько Comparator как константы. Большинство операций сортировки и отсортированные коллекции берут компаратор в качестве параметра.

Ответ 14

//here threshold,buyRange,targetPercentage are three keys on that i have sorted my arraylist 
final Comparator<BasicDBObject> 

    sortOrder = new Comparator<BasicDBObject>() {
                    public int compare(BasicDBObject e1, BasicDBObject e2) {
                        int threshold = new Double(e1.getDouble("threshold"))
                        .compareTo(new Double(e2.getDouble("threshold")));
                        if (threshold != 0)
                            return threshold;

                        int buyRange = new Double(e1.getDouble("buyRange"))
                        .compareTo(new Double(e2.getDouble("buyRange")));
                        if (buyRange != 0)
                            return buyRange;

                        return (new Double(e1.getDouble("targetPercentage")) < new Double(
                                e2.getDouble("targetPercentage")) ? -1 : (new Double(
                                        e1.getDouble("targetPercentage")) == new Double(
                                                e2.getDouble("targetPercentage")) ? 0 : 1));
                    }
                };
                Collections.sort(objectList, sortOrder);

Ответ 15

Легко сравнить два объекта с методом hashcode в java`

public class Sample{

  String a=null;
  String b=null;

  public Sample(){
      a="s";
      b="a";
  }
  public Sample(String a,String b){
      this.a=a;
      this.b=b;
  }
  public static void main(String args[]){
      Sample f=new Sample("b","12");
      Sample s=new Sample("b","12");
      //will return true
      System.out.println((s.a.hashCode()+s.b.hashCode())==(f.a.hashCode()+f.b.hashCode()));

      //will return false
      Sample f=new Sample("b","12");
      Sample s=new Sample("b","13");
      System.out.println((s.a.hashCode()+s.b.hashCode())==(f.a.hashCode()+f.b.hashCode()));

}

Ответ 16

Если вы реализуете интерфейс Comparable, вам нужно выбрать одно простое свойство для заказа. Это известно как естественный порядок. Подумайте об этом как о по умолчанию. Он всегда используется, когда не поставляется конкретный компаратор. Обычно это имя, но ваш прецедент может потребовать что-то другое. Вы можете использовать любое количество других компараторов, которые вы можете предоставить различным API-интерфейсам коллекций, чтобы переопределить естественный порядок.

Также обратите внимание, что обычно, если a.compareTo(b) == 0, то a.equals(b) == true. Это нормально, если нет, но есть побочные эффекты, о которых нужно знать. Посмотрите превосходные javadocs на интерфейсе Comparable, и вы найдете много полезной информации об этом.

Ответ 17

В следующем блоге показан пример с хорошим сочетанием компаратора

http://www.codejava.net/java-core/collections/sorting-a-list-by-multiple-attributes-example

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

/**
 * This is a chained comparator that is used to sort a list by multiple
 * attributes by chaining a sequence of comparators of individual fields
 * together.
 *
 */
public class EmployeeChainedComparator implements Comparator<Employee> {

    private List<Comparator<Employee>> listComparators;

    @SafeVarargs
    public EmployeeChainedComparator(Comparator<Employee>... comparators) {
        this.listComparators = Arrays.asList(comparators);
    }

    @Override
    public int compare(Employee emp1, Employee emp2) {
        for (Comparator<Employee> comparator : listComparators) {
            int result = comparator.compare(emp1, emp2);
            if (result != 0) {
                return result;
            }
        }
        return 0;
    }
}

Вызывающий компаратор:

Collections.sort(listEmployees, new EmployeeChainedComparator(
                new EmployeeJobTitleComparator(),
                new EmployeeAgeComparator(),
                new EmployeeSalaryComparator())
        );

Ответ 18

Начиная с Steve answer, можно использовать тернарный оператор:

public int compareTo(Person other) {
    int f = firstName.compareTo(other.firstName);
    int l = lastName.compareTo(other.lastName);
    return f != 0 ? f : l != 0 ? l : Integer.compare(age, other.age);
}

Ответ 19

//Following is the example in jdk 1.8
package com;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

class User {
    private String firstName;
    private String lastName;
    private Integer age;

    public Integer getAge() {
        return age;
    }

    public User setAge(Integer age) {
        this.age = age;
        return this;
    }

    public String getFirstName() {
        return firstName;
    }

    public User setFirstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public String getLastName() {
        return lastName;
    }

    public User setLastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

}

public class MultiFieldsComparision {

    public static void main(String[] args) {
        List<User> users = new ArrayList<User>();

        User u1 = new User().setFirstName("Pawan").setLastName("Singh").setAge(38);
        User u2 = new User().setFirstName("Pawan").setLastName("Payal").setAge(37);
        User u3 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(60);
        User u4 = new User().setFirstName("Anuj").setLastName("Kumar").setAge(43);
        User u5 = new User().setFirstName("Pawan").setLastName("Chamoli").setAge(44);
        User u6 = new User().setFirstName("Pawan").setLastName("Singh").setAge(5);

        users.add(u1);
        users.add(u2);
        users.add(u3);
        users.add(u4);
        users.add(u5);
        users.add(u6);

        System.out.println("****** Before Sorting ******");

        users.forEach(user -> {
            System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge());
        });

        System.out.println("****** Aftre Sorting ******");

        users.sort(
                Comparator.comparing(User::getFirstName).thenComparing(User::getLastName).thenComparing(User::getAge));

        users.forEach(user -> {
            System.out.println(user.getFirstName() + " , " + user.getLastName() + " , " + user.getAge());
        });

    }

}

Ответ 20

Обычно я переопределяю свой метод compareTo() как это бывает, когда мне приходится выполнять многоуровневую сортировку.

public int compareTo(Song o) {
    // TODO Auto-generated method stub
    int comp1 = 10000000*(movie.compareTo(o.movie))+1000*(artist.compareTo(o.artist))+songLength;
    int comp2 = 10000000*(o.movie.compareTo(movie))+1000*(o.artist.compareTo(artist))+o.songLength;
    return comp1-comp2;
} 

Здесь первое предпочтение отдается имени фильма, а затем художнику и, наконец, songLength. Вам просто нужно убедиться, что эти множители достаточно отдалены, чтобы не пересекать границы друг друга.