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

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()等)是混乱的。

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


当前回答

在我看来,为这样的用例手动编写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。

希望这能有所帮助。

其他回答

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

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

如果用户有多种方式对人排序,你也可以在某个地方设置多个comparator常量。大多数排序操作和排序集合都以比较器作为参数。

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

我认为如果你的比较算法是“聪明的”,那就更令人困惑了。我会选择你建议的众多比较方法。

对我来说唯一的例外就是平等。对于单元测试,重写. equals(在.net中)对我来说很有用,以便确定两个对象之间的几个字段是否相等(而不是引用是否相等)。