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

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

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(), etc...) загроможден на мой взгляд.

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

21 ответов


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


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

вы должны реализовать 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);
}

(от дом код)

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

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

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

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

способ бонус

Я нашел хорошее решение, которое объединяет несколько компараторов в порядке приоритета на 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));
    }
}

конечно Apache Commons коллекции имеет util для этого уже:

ComparatorUtils.chainedComparator (comparatorCollection)

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

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

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

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

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

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


другой вариант, который вы всегда можете рассмотреть, - 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();
}

вы также можете посмотреть перечисление, которое реализует Comparator.

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

например

Collections.sort(myChildren, Child.Order.ByAge.descending());

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

Я искал эквивалент 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);
    }
}

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

  • нет повторного использования кода. Нарушает сухой.
  • шаблонный.
  • повышенная вероятность ошибок.

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

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

обозначим предложение " type A поддерживает сравнение" по Ord A. (От программа перспектива, вы можете думать о Ord A как объект, содержащий логику сравнения двух As. Да, прямо как 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 to A (вызовите эту функцию преобразования f), то вы также можете иметь Ord B. Как? Ну, когда два B экземпляров для сравните, вы сначала преобразуете их в A используя f и затем применить Ord A.

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

Ord A, (B → A) comapOrd B

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


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

у вас есть тип данных с именем Person это включает в себя три поля типа String.

  • известно, что Ord String. Заявление 1, Ord (String, String, String).

  • мы можем легко написать функцию Person to (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.

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


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


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

}

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

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


Если есть несколько способов, пользователь может заказать человека, вы также можете иметь несколько компараторs настройка как константы где-то. Большинство операций сортировки и отсортированных коллекций принимают компаратор в качестве параметра.


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

легко сравнить два объекта с методом 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()));

}

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

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


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

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

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

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

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

    }

}

обычно я переопределяю мой 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. Вы просто должны убедиться, что эти множители достаточно далеко, чтобы не пересекать границы друг друга.


это очень легко сделать с помощью библиотека гуава Google.

например Objects.equal(name, name2) && Objects.equal(age, age2) && ...

примеры: