列表l可以按如下分类。

  1. Collections.sort(l);

如果ListString元素组成,则将按字母顺序排序。如果它由Date元素组成,则将按时间顺序排序。这是怎么发生的? StringDate都实现[Comparable](https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html)接口。Comparable实现方式为类提供了自然的排序,从而可以对该类的对象进行自动排序。下表总结了一些实现Comparable的更重要的Java平台类。
类实现可比

自然排序
Byte 签名数字
Character 无符号数值
Long 签名数字
Integer 签名数字
Short 签名数字
Double 签名数字
Float 签名数字
BigInteger 签名数字
BigDecimal 签名数字
Boolean Boolean.FALSE < Boolean.TRUE
File 路径名依赖于系统的词典
String 词典
Date 年表
CollationKey 特定于语言环境的词典

如果尝试对列表进行排序,但其元素未实现ComparableCollections.sort(list)将抛出ClassCastException。同样,如果您尝试对无法使用比较器(comparator)将元素进行相互比较的列表进行排序,则Collections.sort(list, comparator)将引发ClassCastException。可以相互比较的元素称为相互可比(mutually comparable。 尽管不同类型的元素可以相互比较,但此处列出的所有类都不允许类间比较。
如果您只想对可比较元素的列表进行排序或创建它们的排序集合,那么您真正需要了解Comparable接口。如果您想实现自己的Comparable类型,那么下一部分将是您感兴趣的。

编写自己的Comparable类型

Comparable接口由以下方法组成。

  1. public interface Comparable<T> {
  2. public int compareTo(T o);
  3. }

compareTo方法将接收对象与指定对象进行比较,并根据接收对象是否小于,等于或大于指定对象而返回负整数,0或正整数。如果无法将指定对象与接收对象进行比较,则该方法将抛出ClassCastException
following class representing a person's name工具Comparable
以下表示一个人的名字的类实现Comparable。

  1. import java.util.*;
  2. public class Name implements Comparable<Name> {
  3. private final String firstName, lastName;
  4. public Name(String firstName, String lastName) {
  5. if (firstName == null || lastName == null)
  6. throw new NullPointerException();
  7. this.firstName = firstName;
  8. this.lastName = lastName;
  9. }
  10. public String firstName() { return firstName; }
  11. public String lastName() { return lastName; }
  12. public boolean equals(Object o) {
  13. if (!(o instanceof Name))
  14. return false;
  15. Name n = (Name) o;
  16. return n.firstName.equals(firstName) && n.lastName.equals(lastName);
  17. }
  18. public int hashCode() {
  19. return 31*firstName.hashCode() + lastName.hashCode();
  20. }
  21. public String toString() {
  22. return firstName + " " + lastName;
  23. }
  24. public int compareTo(Name n) {
  25. int lastCmp = lastName.compareTo(n.lastName);
  26. return (lastCmp != 0 ? lastCmp : firstName.compareTo(n.firstName));
  27. }
  28. }

为了使前面的示例简短,该类在某种程度上受到限制:它不支持中间名,它需要名字和姓氏,并且不以任何方式进行国际化。但是,它说明了以下重要点:

  • Name对象是不可变的。在所有其他条件相同的情况下,不变类型是必经之路,特别是对于将用作Sets中的元素或Maps中的键的对象。如果您在集合中修改其元素或键,则这些集合将中断。
  • 构造函数检查其参数是否为null。这样可确保所有Name对象格式正确,以便其他任何方法都不会抛出NullPointerException
  • hashCode方法被重新定义。这对于重新定义equals方法的任何类都是必不可少的。(相等的对象必须具有相等的哈希码。)
  • 如果指定的对象为null或类型不正确,则equals方法返回false。在这些情况下,compareTo方法将抛出运行时异常。这两种行为都是相应方法的一般合同所要求的。
  • toString方法已重新定义,因此它以易于阅读的形式打印Name。这总是一个好主意,尤其是对于将要放入集合中的对象。各种集合类型的toString方法取决于其元素,键和值的toString方法。

由于本节是关于元素排序的,所以让我们再谈谈有关NamecompareTo方法。它实现了标准的名称排序算法,其中姓氏优先于名字。这正是您想要的自然顺序。如果自然排序不自然,那确实会造成混乱!
看看compareTo是如何实现的,因为它很典型。首先,您比较对象的最高效部分(在本例中为姓)。通常,您可以只使用部分类型的自然顺序。在这种情况下,该部分是一个字符串,自然顺序(字典顺序)正是所需要的。如果比较结果等于零,则表示相等,那么您就可以完成:您只需返回结果即可。如果最重要的部分相等,则继续比较下一个最重要的部分。在这种情况下,只有两个部分——名和姓。如果有更多的部分,则将以明显的方式进行操作,比较各部分,直到找到两个不相等的部分,或者您正在比较最低有效部分,此时将返回比较结果。
只是为了展示所有功能,这里有一个程序可以构建名称列表并对它们进行排序。

  1. import java.util.*;
  2. public class NameSort {
  3. public static void main(String[] args) {
  4. Name nameArray[] = {
  5. new Name("John", "Smith"),
  6. new Name("Karl", "Ng"),
  7. new Name("Jeff", "Smith"),
  8. new Name("Tom", "Rich")
  9. };
  10. List<Name> names = Arrays.asList(nameArray);
  11. Collections.sort(names);
  12. System.out.println(names);
  13. }
  14. }

如果您运行此程序,则显示的内容如下。

  1. [Karl Ng, Tom Rich, Jeff Smith, John Smith]

compareTo方法的行为有四个限制,我们现在不再赘述,因为它们是相当技术性和无聊的,最好留在API文档中。 所有实现Comparable的类都必须遵守这些限制,这一点非常重要,因此,如果要编写实现该类的类,请阅读Comparable的文档。尝试对违反限制的对象列表进行排序具有未定义的行为。从技术上讲,这些限制可确保自然顺序是实现该顺序的类的对象的总顺序; 这是确保正确定义排序所必需的。

比较器

如果要按自然顺序以外的顺序对某些对象进行排序怎么办? 或者,如果您想对一些未实现Comparable的对象进行排序,该怎么办?要执行这两项操作,您需要提供一个Comparator(封装顺序的对象)。与Comparable接口类似,Comparator接口由一个方法组成。

  1. public interface Comparator<T> {
  2. int compare(T o1, T o2);
  3. }

关于Comparable的大部分说法也适用于Comparator。编写compare方法与编写compareTo方法几乎相同,除了前者将两个对象都作为参数传递。出于相同的原因,compare方法必须遵守与ComparablecompareTo方法相同的四个技术限制——Comparator必须在其比较的对象上引发总顺序。
假设您有一个名为Employee的类,如下所示。

  1. public class Employee implements Comparable<Employee> {
  2. public Name name() { ... }
  3. public int number() { ... }
  4. public Date hireDate() { ... }
  5. ...
  6. }

假设Employee实例的自然排序是对雇员姓名的名称排序(如上例中所定义)。不幸的是,老板要求按年长顺序列出员工名单。这意味着我们必须做一些工作,但不多做。以下程序将生成所需的列表。

  1. import java.util.*;
  2. public class EmpSort {
  3. static final Comparator<Employee> SENIORITY_ORDER =
  4. new Comparator<Employee>() {
  5. public int compare(Employee e1, Employee e2) {
  6. return e2.hireDate().compareTo(e1.hireDate());
  7. }
  8. };
  9. // Employee database
  10. static final Collection<Employee> employees = ... ;
  11. public static void main(String[] args) {
  12. List<Employee> e = new ArrayList<Employee>(employees);
  13. Collections.sort(e, SENIORITY_ORDER);
  14. System.out.println(e);
  15. }
  16. }

程序中的Comparator相当简单。它依赖于应用于hireDate访问器方法返回的值的Date的自然顺序。请注意,Comparator将第二个参数的聘用日期传递给第一个参数,而不是相反。原因是最近雇用的雇员的最低年龄;按聘用日期的顺序进行排序会将列表按相反的年资顺序排列。人们有时用来实现此效果的另一种技术是保持参数顺序,但否定比较结果。

  1. // Don't do this!!
  2. return -r1.hireDate().compareTo(r2.hireDate());

您应该始终使用前一种技术来支持后者,因为不能保证后者可以工作。这样做的原因是,如果compareTo方法的参数小于调用它的对象,则可以返回任何负整数。有一个负整数在被否定时仍为负,看起来很奇怪。

  1. -Integer.MIN_VALUE == Integer.MIN_VALUE

前面程序中的Comparator可以很好地用于对List进行排序,但是它确实有一个不足:它不能用于对排序的集合(如TreeSet)进行排序,因为它生成的排序与equals不兼容。 这意味着此Comparator等于,equals方法不等于的对象。特别是,在同一日期雇用的任何两名雇员将进行比较。在对List进行排序时,这无关紧要;但是,当您使用Comparator安排已排序的集合时,这是致命的。如果使用此Comparator将同一日期雇用的多名员工插入TreeSet,则只会将第一个雇员添加到该Set中; 第二个将被视为重复元素,将被忽略。
要解决此问题,只需调整Comparator,使其产生与equals兼容的排序。换句话说,对其进行调整,以使使用compare时,被视为相等的唯一元素是使用compare时也被视为相等的元素。这样做的方法是执行两部分比较(就Name而言),其中第一部分是我们感兴趣的部分(在本例中为雇用日期),第二部分是唯一标识对象的属性。在这里,员工人数是显而易见的属性。这就是产生的Comparator

  1. static final Comparator<Employee> SENIORITY_ORDER =
  2. new Comparator<Employee>() {
  3. public int compare(Employee e1, Employee e2) {
  4. int dateCmp = e2.hireDate().compareTo(e1.hireDate());
  5. if (dateCmp != 0)
  6. return dateCmp;
  7. return (e1.number() < e2.number() ? -1 :
  8. (e1.number() == e2.number() ? 0 : 1));
  9. }
  10. };

最后一点:您可能会想用以下更简单的方法替换Comparator中的最终return语句:

  1. return e1.number() - e2.number();

除非您绝对确定没有人会有负号,否则请不要这样做!该技巧通常不起作用,因为有符号整数类型不足以表示两个任意有符号整数的差。如果i是一个大的正整数,而j是一个大的负整数,则i - j将溢出并返回一个负整数。生成的comparator违反了我们一直在谈论的四个技术限制(传递性)之一,并产生了可怕的,微妙的错误。 这不是纯粹的理论问题; 人们被它烧死了。