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


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

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   169  

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

Если класс реализует сопоставимый, то такой компаратор может быть использован в методе compareTo:

@Override
public int compareTo(Person o){
    return Comparator.comparing(Person::getFirstName)
              .thenComparing(Person::getLastName)
              .thenComparingInt(Person::getAge)
              .compare(this, o);
}

вы должны реализовать Comparable <Person>. Предполагая, что все поля не будут null (для простоты), что age является int, а compare ranking-это first, last, age, The 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(); 
        */  
    }  
});  

это намного лучше, но требует некоторого кода котельной плиты для наиболее распространенного случая использования: нулевые значения должны быть оценены меньше по умолчанию. Для null-полей вы должны предоставить дополнительную директиву 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 Collections имеет util для этого уже:

ComparatorUtils.chainedComparator (comparatorCollection)

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

@Patrick для сортировки более одного поля последовательно попробуйте 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();
}

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

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 вручную для такого случая использования является ужасным решением ИМО. Такие специальные подходы имеют много недостатков:

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

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

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

обозначим предложение " тип 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).

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

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 является заводским методом для создания продуктов.
  • все выражение соответствует утверждению 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.equals (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) && ...

примеры: