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

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中使用hashcode方法比较两个对象很容易。

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

}

其他回答

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

对于那些能够使用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);
    }
}

(来自Java中基于多个字段对对象列表进行排序的方法)

工作代码在这个要点

使用Java 8 lambda(2019年4月10日添加)

Java 8通过lambda很好地解决了这个问题(尽管Guava和Apache Commons可能仍然提供了更大的灵活性):

Collections.sort(reportList, Comparator.comparing(Report::getReportKey)
            .thenComparing(Report::getStudentNumber)
            .thenComparing(Report::getSchool));

感谢@高公的回答。

杂乱而复杂:手工分类

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

显然,这更简洁,但更容易出错,因为使用string而失去了对字段的直接引用(没有类型安全,自动重构)。现在,如果字段被重命名,编译器甚至不会报告问题。此外,由于该解决方案使用反射,排序要慢得多。

到达那里:排序谷歌番石榴的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();  
    }  
});  

像Guava的ComparisonChain一样,这个库类很容易在多个字段上排序,但也为空值定义了默认行为。1, a, b, z, null)。但是,您也不能指定任何其他内容,除非您提供自己的Comparator。

Thus

最终,这归结于口味和灵活性的需要(Guava的ComparisonChain) vs.简洁的代码(Apache的CompareToBuilder)。

额外的方法

我发现了一个很好的解决方案,在MultiComparator的CodeReview中按优先级顺序组合多个比较器:

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

晚做总比不到好——如果你正在寻找不必要的混乱或开销,那么很难同时在最少的代码/快速执行方面击败下面的方法。

数据类:

public class MyData {
    int id;
    boolean relevant;
    String name;
    float value;
}

比较器:

public class MultiFieldComparator implements Comparator<MyData> {
    @Override
    public int compare(MyData dataA, MyData dataB) {
        int result;
        if((result = Integer.compare(dataA.id, dataB.id)) == 0 &&
           (result = Boolean.compare(dataA.relevant, dataB.relevant)) == 0 &&
           (result = dataA.name.compareTo(dataB.name)) == 0)
            result = Float.compare(dataA.value, dataB.value);
        return result;
    }
}

如果你只是想按自定义顺序对集合进行排序,那么下面的代码就更清晰了:

myDataList.sort((dataA, dataB) -> {
    int result;
    if((result = Integer.compare(dataA.id, dataB.id)) == 0 &&
       (result = Boolean.compare(dataA.relevant, dataB.relevant)) == 0 &&
       (result = dataA.name.compareTo(dataB.name)) == 0)
        result = Float.compare(dataA.value, dataB.value);
    return result;
});

另一个可以考虑的选项是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();
}