列表l
可以按如下分类。
Collections.sort(l);
如果List
由String
元素组成,则将按字母顺序排序。如果它由Date
元素组成,则将按时间顺序排序。这是怎么发生的? String
和Date
都实现[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 |
特定于语言环境的词典 |
如果尝试对列表进行排序,但其元素未实现Comparable
,Collections.sort(list)
将抛出ClassCastException
。同样,如果您尝试对无法使用比较器(comparator
)将元素进行相互比较的列表进行排序,则Collections.sort(list, comparator)
将引发ClassCastException
。可以相互比较的元素称为相互可比(mutually comparable)。 尽管不同类型的元素可以相互比较,但此处列出的所有类都不允许类间比较。
如果您只想对可比较元素的列表进行排序或创建它们的排序集合,那么您真正需要了解Comparable
接口。如果您想实现自己的Comparable
类型,那么下一部分将是您感兴趣的。
编写自己的Comparable
类型
Comparable
接口由以下方法组成。
public interface Comparable<T> {
public int compareTo(T o);
}
compareTo
方法将接收对象与指定对象进行比较,并根据接收对象是否小于,等于或大于指定对象而返回负整数,0或正整数。如果无法将指定对象与接收对象进行比较,则该方法将抛出ClassCastException
。
该 following class representing a person's name
工具Comparable
。
以下表示一个人的名字的类实现Comparable。
import java.util.*;
public class Name implements Comparable<Name> {
private final String firstName, lastName;
public Name(String firstName, String lastName) {
if (firstName == null || lastName == null)
throw new NullPointerException();
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() { return firstName; }
public String lastName() { return lastName; }
public boolean equals(Object o) {
if (!(o instanceof Name))
return false;
Name n = (Name) o;
return n.firstName.equals(firstName) && n.lastName.equals(lastName);
}
public int hashCode() {
return 31*firstName.hashCode() + lastName.hashCode();
}
public String toString() {
return firstName + " " + lastName;
}
public int compareTo(Name n) {
int lastCmp = lastName.compareTo(n.lastName);
return (lastCmp != 0 ? lastCmp : firstName.compareTo(n.firstName));
}
}
为了使前面的示例简短,该类在某种程度上受到限制:它不支持中间名,它需要名字和姓氏,并且不以任何方式进行国际化。但是,它说明了以下重要点:
Name
对象是不可变的。在所有其他条件相同的情况下,不变类型是必经之路,特别是对于将用作Sets
中的元素或Maps
中的键的对象。如果您在集合中修改其元素或键,则这些集合将中断。- 构造函数检查其参数是否为
null
。这样可确保所有Name
对象格式正确,以便其他任何方法都不会抛出NullPointerException
。 hashCode
方法被重新定义。这对于重新定义equals
方法的任何类都是必不可少的。(相等的对象必须具有相等的哈希码。)- 如果指定的对象为
null
或类型不正确,则equals
方法返回false
。在这些情况下,compareTo
方法将抛出运行时异常。这两种行为都是相应方法的一般合同所要求的。 toString
方法已重新定义,因此它以易于阅读的形式打印Name
。这总是一个好主意,尤其是对于将要放入集合中的对象。各种集合类型的toString
方法取决于其元素,键和值的toString
方法。
由于本节是关于元素排序的,所以让我们再谈谈有关Name
的compareTo
方法。它实现了标准的名称排序算法,其中姓氏优先于名字。这正是您想要的自然顺序。如果自然排序不自然,那确实会造成混乱!
看看compareTo
是如何实现的,因为它很典型。首先,您比较对象的最高效部分(在本例中为姓)。通常,您可以只使用部分类型的自然顺序。在这种情况下,该部分是一个字符串,自然顺序(字典顺序)正是所需要的。如果比较结果等于零,则表示相等,那么您就可以完成:您只需返回结果即可。如果最重要的部分相等,则继续比较下一个最重要的部分。在这种情况下,只有两个部分——名和姓。如果有更多的部分,则将以明显的方式进行操作,比较各部分,直到找到两个不相等的部分,或者您正在比较最低有效部分,此时将返回比较结果。
只是为了展示所有功能,这里有一个程序可以构建名称列表并对它们进行排序。
import java.util.*;
public class NameSort {
public static void main(String[] args) {
Name nameArray[] = {
new Name("John", "Smith"),
new Name("Karl", "Ng"),
new Name("Jeff", "Smith"),
new Name("Tom", "Rich")
};
List<Name> names = Arrays.asList(nameArray);
Collections.sort(names);
System.out.println(names);
}
}
如果您运行此程序,则显示的内容如下。
[Karl Ng, Tom Rich, Jeff Smith, John Smith]
对compareTo
方法的行为有四个限制,我们现在不再赘述,因为它们是相当技术性和无聊的,最好留在API文档中。 所有实现Comparable
的类都必须遵守这些限制,这一点非常重要,因此,如果要编写实现该类的类,请阅读Comparable
的文档。尝试对违反限制的对象列表进行排序具有未定义的行为。从技术上讲,这些限制可确保自然顺序是实现该顺序的类的对象的总顺序; 这是确保正确定义排序所必需的。
比较器
如果要按自然顺序以外的顺序对某些对象进行排序怎么办? 或者,如果您想对一些未实现Comparable
的对象进行排序,该怎么办?要执行这两项操作,您需要提供一个Comparator
(封装顺序的对象)。与Comparable
接口类似,Comparator
接口由一个方法组成。
public interface Comparator<T> {
int compare(T o1, T o2);
}
关于Comparable
的大部分说法也适用于Comparator
。编写compare
方法与编写compareTo
方法几乎相同,除了前者将两个对象都作为参数传递。出于相同的原因,compare
方法必须遵守与Comparable
的compareTo
方法相同的四个技术限制——Comparator
必须在其比较的对象上引发总顺序。
假设您有一个名为Employee
的类,如下所示。
public class Employee implements Comparable<Employee> {
public Name name() { ... }
public int number() { ... }
public Date hireDate() { ... }
...
}
假设Employee
实例的自然排序是对雇员姓名的名称排序(如上例中所定义)。不幸的是,老板要求按年长顺序列出员工名单。这意味着我们必须做一些工作,但不多做。以下程序将生成所需的列表。
import java.util.*;
public class EmpSort {
static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
return e2.hireDate().compareTo(e1.hireDate());
}
};
// Employee database
static final Collection<Employee> employees = ... ;
public static void main(String[] args) {
List<Employee> e = new ArrayList<Employee>(employees);
Collections.sort(e, SENIORITY_ORDER);
System.out.println(e);
}
}
程序中的Comparator
相当简单。它依赖于应用于hireDate
访问器方法返回的值的Date
的自然顺序。请注意,Comparator
将第二个参数的聘用日期传递给第一个参数,而不是相反。原因是最近雇用的雇员的最低年龄;按聘用日期的顺序进行排序会将列表按相反的年资顺序排列。人们有时用来实现此效果的另一种技术是保持参数顺序,但否定比较结果。
// Don't do this!!
return -r1.hireDate().compareTo(r2.hireDate());
您应该始终使用前一种技术来支持后者,因为不能保证后者可以工作。这样做的原因是,如果compareTo
方法的参数小于调用它的对象,则可以返回任何负整数。有一个负整数在被否定时仍为负,看起来很奇怪。
-Integer.MIN_VALUE == Integer.MIN_VALUE
前面程序中的Comparator
可以很好地用于对List
进行排序,但是它确实有一个不足:它不能用于对排序的集合(如TreeSet
)进行排序,因为它生成的排序与equals
不兼容。 这意味着此Comparator
等于,equals
方法不等于的对象。特别是,在同一日期雇用的任何两名雇员将进行比较。在对List
进行排序时,这无关紧要;但是,当您使用Comparator
安排已排序的集合时,这是致命的。如果使用此Comparator
将同一日期雇用的多名员工插入TreeSet
,则只会将第一个雇员添加到该Set
中; 第二个将被视为重复元素,将被忽略。
要解决此问题,只需调整Comparator
,使其产生与equals
兼容的排序。换句话说,对其进行调整,以使使用compare
时,被视为相等的唯一元素是使用compare
时也被视为相等的元素。这样做的方法是执行两部分比较(就Name
而言),其中第一部分是我们感兴趣的部分(在本例中为雇用日期),第二部分是唯一标识对象的属性。在这里,员工人数是显而易见的属性。这就是产生的Comparator
。
static final Comparator<Employee> SENIORITY_ORDER =
new Comparator<Employee>() {
public int compare(Employee e1, Employee e2) {
int dateCmp = e2.hireDate().compareTo(e1.hireDate());
if (dateCmp != 0)
return dateCmp;
return (e1.number() < e2.number() ? -1 :
(e1.number() == e2.number() ? 0 : 1));
}
};
最后一点:您可能会想用以下更简单的方法替换Comparator
中的最终return
语句:
return e1.number() - e2.number();
除非您绝对确定没有人会有负号,否则请不要这样做!该技巧通常不起作用,因为有符号整数类型不足以表示两个任意有符号整数的差。如果i
是一个大的正整数,而j
是一个大的负整数,则i - j
将溢出并返回一个负整数。生成的comparator
违反了我们一直在谈论的四个技术限制(传递性)之一,并产生了可怕的,微妙的错误。 这不是纯粹的理论问题; 人们被它烧死了。