假设你有一些对象,它们有几个字段可以比较:

public class Person {

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

    /* Constructors */

    /* Methods */

}

所以在这个例子中,当你问if:

a.compareTo(b) > 0

你可能会问a的姓是不是在b的姓之前,或者a的年龄是不是比b大,等等……

在不增加不必要的混乱或开销的情况下,在这些类型的对象之间进行多重比较的最干净的方法是什么?

comparable接口只允许通过一个字段进行比较 在我看来,添加大量的比较方法(如compareByFirstName(), compareByAge()等)是混乱的。

那么最好的解决办法是什么呢?


当前回答

对于那些能够使用Java 8流API的人来说,这里有一个更整洁的方法: lambda和排序

我正在寻找相当于c# LINQ:

.ThenBy(...)

我在Comparator上找到了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);
    }
}

其他回答

您应该实现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);
}

与比较方法不同,您可能只想在Person类中定义几种类型的“Comparator”子类。这样就可以将它们传递到标准的Collections排序方法中。

在我看来,为这样的用例手动编写Comparator是一个糟糕的解决方案。这种特别的方法有很多缺点:

没有代码重用。违反了干。 样板。 增加了出错的可能性。


那么解决方案是什么呢?

首先是一些理论。

让我们用Ord A来表示命题“type A支持比较”(从程序的角度来看,您可以将Ord A看作一个包含比较两个A的逻辑的对象。是的,就像Comparator一样。)

现在,如果Ord A和Ord B,那么它们的合成(A, B)也应该支持比较。例如,Ord (A, B)。如果Ord A, Ord B,和Ord C,那么Ord (A, B, C)。

我们可以将这个论证扩展到任意性,并说:

A站,B站,C站,……, Ord Z⇒Ord (A, B, C, .., Z)

我们称这个为表述一。

复合材料的比较将像您在问题中描述的那样工作:首先尝试第一个比较,然后是下一个比较,然后是下一个比较,依此类推。

这是解的第一部分。现在是第二部分。

如果你知道A字词,也知道如何将B转换为A(称之为转换函数f),那么你也可以有B字词,怎么做?当要比较两个B实例时,首先使用f将它们转换为A,然后应用Ord A。

在这里,我们将变换B→A映射到Ord A→Ord B,这被称为逆变映射(或简称comap)。

Ord A, (B→A)

我们称这个为表述二。


现在让我们把这个应用到你的例子中。

您有一个名为Person的数据类型,它包含三个String类型的字段。

我们知道Ord String。通过语句1,Ord(字符串,字符串,字符串)。 我们可以很容易地编写一个从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>>的方法。这对应于表述一。(P3代表三要素产品。Product是复合材料的代数术语。) Comap对应于Comap。 F<A, B>表示变换函数A→B。 P是创建产品的工厂方法。 整个表达式对应于表述2。

希望这能有所帮助。

下面的博客给出了一个很好的链式比较器的例子

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

使用java8:

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