对象排序
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/order.html
一个List
l
可以按以下方式排序。
Collections.sort(l);
如果List
包含String
元素,则将按字母顺序对其进行排序。如果包含Date
元素,则将按时间顺序对其进行排序。这是如何发生的呢?String
和Date
都实现了Comparable
接口。
Comparable实现为一个类提供了*自然排序*,允许该类的对象自动排序。下表总结了一些更重要的实现
Comparable`接口的 Java 平台类。
实现 Comparable 接口的类
类 | 自然排序 |
---|---|
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
。可以相互比较的元素被称为可相互比较的。尽管不同类型的元素可能是可相互比较的,但这里列出的类中没有一个允许跨类比较。
如果你只想对可比较元素的列表进行排序或创建排序的集合,那么关于Comparable
接口,这就是你真正需要知道的全部内容。如果你想要实现自己的Comparable
类型,那么下一节将对你感兴趣。
编写自己的可比较类型
Comparable
接口包含以下方法。
public interface Comparable<T> {
public int compareTo(T o);
}
compareTo
方法比较接收对象与指定对象,并根据接收对象是小于、等于还是大于指定对象返回负整数、0 或正整数。如果指定对象无法与接收对象比较,则该方法会抛出ClassCastException
。
下面的类代表一个人的名字,实现了Comparable
。examples/Name.java
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
对象是不可变的。其他条件相同的情况下,不可变类型是最好的选择,特别是对于将用作Set
中的元素或Map
中的键的对象。如果在集合中修改它们的元素或键,这些集合将会中断。 - 构造函数检查其参数是否为
null
。这确保所有的Name
对象都是格式良好的,以便其他方法永远不会抛出NullPointerException
。 -
hashCode
方法被重新定义。这对于重新定义equals
方法的任何类都是必不可少的。(相等的对象必须具有相等的哈希码。) -
equals
方法在指定对象为null
或不合适类型时返回false
。compareTo
方法在这些情况下会抛出运行时异常。这两种行为都是各自方法的一般契约所要求的。 -
toString
方法已被重新定义,以便以人类可读的形式打印Name
。这总是一个好主意,特别是对于将要放入集合中的对象。各种集合类型的toString
方法依赖于它们的元素、键和值的toString
方法。
由于本节是关于元素排序的,让我们再谈一下 Name
的 compareTo
方法。它实现了标准的名称排序算法,其中姓氏优先于名字。这正是你在自然排序中想要的。如果自然排序是不自然的,那将会非常令人困惑!
看一下 compareTo
的实现方式,因为它是相当典型的。首先,你比较对象的最重要部分(在这种情况下是姓)。通常情况下,你可以直接使用部分类型的自然排序。在这种情况下,部分是一个 String
,自然(词典)排序正是所需的。如果比较结果不是零,表示相等,你就完成了:只需返回结果。如果最重要的部分相等,你继续比较下一个最重要的部分。在这种情况下,只有两个部分——名和姓。如果有更多的部分,你会按照明显的方式继续,比较部分直到找到两个不相等的部分或者你正在比较最不重要的部分,此时你会返回比较的结果。
为了展示它是如何工作的,这里是一个构建名称列表并对其进行排序的程序。
代码语言:javascript复制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);
}
}
如果你运行这个程序,这是它打印的内容。
代码语言:javascript复制[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);
}
compare
方法比较其两个参数,根据第一个参数是否小于、等于或大于第二个参数返回负整数、0 或正整数。如果任一参数的类型对于Comparator
不合适,则compare
方法会抛出ClassCastException
。
大部分关于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
实例的自然排序是根据员工姓名(如前面的示例中定义的)的Name
排序。不幸的是,老板要求按资历顺序列出员工名单。这意味着我们需要做一些工作,但不多。以下程序将生成所需的列表。
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
方法如果其参数小于调用它的对象,则可以返回任何负的int
。有一个负的int
在取反后仍然是负的,尽管这看起来很奇怪。
-Integer.MIN_VALUE == Integer.MIN_VALUE
前面程序中的Comparator
用于对List
进行排序很好,但它有一个缺陷:它不能用于对已排序的集合(如TreeSet
)进行排序,因为它生成的排序与equals
不兼容。这意味着这个Comparator
将把equals
方法不认为相等的对象等同起来。特别是,任何在同一天入职的两名员工将被视为相等。当你对List
进行排序时,这并不重要;但当你使用Comparator
对已排序的集合进行排序时,这是致命的。如果你使用这个Comparator
将多名在同一天入职的员工插入TreeSet
,只有第一个会被添加到集合中;第二个将被视为重复元素并被忽略。
要解决这个问题,只需微调Comparator
,使其生成一个与equals
兼容的排序。换句话说,调整它使得当使用compare
进行比较时,只有那些在使用equals
进行比较时也被视为相等的元素才被视为相等。做法是执行一个两部分比较(如对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
违反了我们一直谈论的四个技术限制之一(传递性),并产生可怕的、微妙的错误。这不仅仅是理论上的担忧;人们会因此受到伤害。
SortedSet 接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/sorted-set.html
SortedSet
是一个按升序维护其元素的集合,根据元素的自然顺序或在 SortedSet
创建时提供的 Comparator
进行排序。除了正常的 Set
操作外,SortedSet
接口还提供以下操作:
-
Range view
— 允许对排序集合进行任意范围操作 -
Endpoints
— 返回排序集合中的第一个或最后一个元素 -
Comparator access
— 返回用于对集合进行排序的Comparator
(如果有的话)
SortedSet
接口的代码如下。
public interface SortedSet<E> extends Set<E> {
// Range-view
SortedSet<E> subSet(E fromElement, E toElement);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement);
// Endpoints
E first();
E last();
// Comparator access
Comparator<? super E> comparator();
}
集合操作
SortedSet
从 Set
继承的操作在排序集合和普通集合上的行为完全相同,但有两个例外:
-
iterator
操作返回的Iterator
按顺序遍历排序集合。 -
toArray
返回的数组按顺序包含了排序后的集合元素。
尽管接口不保证,但 Java 平台的 SortedSet
实现的 toString
方法返回一个包含排序集合所有元素的字符串,按顺序排列。
标准构造函数
按照惯例,所有通用的 Collection
实现都提供一个标准的转换构造函数,接受一个 Collection
;SortedSet
实现也不例外。在 TreeSet
中,这个构造函数创建一个根据元素的自然顺序排序的实例。这可能是一个错误。最好动态检查指定的集合是否是 SortedSet
实例,如果是,则根据相同的标准(比较器或自然顺序)对新的 TreeSet
进行排序。因为 TreeSet
采取了它的方法,它还提供一个接受 SortedSet
的构造函数,并返回一个包含相同元素并根据相同标准排序的新 TreeSet
。请注意,参数的编译时类型,而不是运行时类型,决定调用这两个构造函数中的哪一个(以及是否保留排序标准)。
SortedSet
实现通常还提供一个构造函数,接受一个 Comparator
并返回一个根据指定 Comparator
排序的空集合。如果传递 null
给这个构造函数,它将返回一个根据元素的自然顺序排序的集合。
范围视图操作
range-view
操作在某种程度上类似于List
接口提供的操作,但有一个重大区别。排序集的范围视图即使在直接修改支持的排序集的情况下仍然有效。这是因为排序集的范围视图的端点是元素空间中的绝对点,而不是备份集合中的特定元素,这对于列表是成立的。排序集的range-view
实际上只是窗口,显示在元素空间的指定部分中集合的任何部分。对排序集的range-view
的更改会写回到支持的排序集中,反之亦然。因此,长时间使用排序集上的range-view
是可以的,不像列表上的range-view
那样。
排序集提供三种range-view
操作。第一种是subSet
,类似于subList
,需要两个端点。端点不是索引,而是对象,并且必须与排序集中的元素可比,使用Set
的Comparator
或其元素的自然排序,取决于Set
用于对自身排序的方式。与subList
类似,范围是半开的,包括其低端点但不包括高端点。
因此,下面这行代码告诉你在名为dictionary
的字符串SortedSet
中包含多少个介于"doorbell"
和"pickle"
之间的单词,包括"doorbell"
但不包括"pickle"
。
int count = dictionary.subSet("doorbell", "pickle").size();
以类似的方式,以下一行代码可以删除所有以字母f
开头的元素。
dictionary.subSet("f", "g").clear();
类似的技巧可以用来打印一个表格,告诉你每个字母开头的单词有多少个。
代码语言:javascript复制for (char ch = 'a'; ch <= 'z'; ) {
String from = String.valueOf(ch );
String to = String.valueOf(ch);
System.out.println(from ": " dictionary.subSet(from, to).size());
}
假设你想查看一个闭区间,其中包含两个端点,而不是一个开区间。如果元素类型允许计算元素空间中给定值的后继,只需请求从lowEndpoint
到successor(highEndpoint)
的subSet
。虽然这并不是完全明显的,但在String
的自然排序中,字符串s
的后继是s "