编写你自己的 comparable 类型
comparable 接口由一个单一的方法构成:
public interface comparable {
public int compareto(object o);
}
compareto 方法将接收对象与特定对象进行比较,并在接收对象小于、等于或大于特定对象时分别返回负整数、空或一个正整数。如果特定对象不能与接收对象相比较,该方法扔出一个classcastexception. 这是一个表示某人姓名的类(a class representing a person"s name), 它实现了 comparable:
import java.util.*;
public class name implements comparable {
private 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(object o) {
name n = (name)o;
int lastcmp = lastname.compareto(n.lastname);
return (lastcmp!=0 ? lastcmp :
firstname.compareto(n.firstname));
}
}
为了使这个例子短一些,该类受到了一点限制:它不支持中间名,它要求必须同时具有first name 和 last name, 而这不是在全世界都通用的。尽管如此,这个例子仍有几个重要之处:
name 对象是不变的( immutable)。作为相等、不变类型的所有其它事情就是如何做的问题,特别是对那些将被用来作为 sets 中的元素或 maps 中的键的对象来说,更是如此。如果你对这些 对象集 中的元素或键做了更改,这些 对象集 将中断。
构造函数可检查它的参数是否为 null。 这可以保证所有的name 对象都能很好地形成。因而没有其它方法会? nullpointerexception.
hashcode 方法被重新定义。对重新定义 equals 方法的任意类来说,这是必需的(essential)。 一般约定(general contract)需要 object.equals. (equal 对象必须具有相等的哈希代码) 。
如果特定对象为 null,或一个不适当的类型, equals 方法则返回 false。 在这种情况下, compareto 方法扔出一个运行时异常。这两个行为都是各自方法的一般约定所必需的。
tostring 方法已被重新定义,从而可以以人们能够读懂的形式打印 name 。这总是一个好主意,特别是对要被放入对象集 中的对象来说,更有益处。各种 对象集 类型的 tostring 方法依赖它们的元素、键和值的 tostring 方法。
由于这一节介绍的是有关元素排序的问题,因而让我们稍微多谈一点 name 的 compareto 方法。它实现标准的姓名-排序算法,在该算法中,last name 优先于 first name。这恰恰是你在一个natural ordering(自然排序)中所想要的。 如果自然排序不自然,那才容易引起混乱呢!
请看 compareto 是如何被实现的,因为它是相当典型的。首先,你将 object 参数转换为适当类型; 如果参数类型是不适当的,则会扔出一个适当的异常(classcastexception);那么你应该比较对象的最重要部分(在此案例中为 last name)。通常,你可以使用该部分的类型的自然排序。 在次案例中,该部分是一个 string, 并且自然的(按词典顺序的)排序正是所要求的。如果比较的结果是空(它表示等同性)之外的其它东西,你就做完了:你可以返回结果。 如果最重要的部分是相等的,你就应该继续比较次重要部分。在此案例中,只有两个部分 (first name and last name)。 如果有更多的部分,你就应该以显而易见的方式继续进行,直到发现两个不相等的部分(否则你就应该比较最不重要的部分),这时,你就可以返回比较结果了。这是 一个建立 name 对象列表并对它们进行排序的小程序:
import java.util.*;
class namesort {
public static void main(string args[]) {
name n[] = {
new name("john", "lennon"),
new name("karl", "marx"),
new name("groucho", "marx"),
new name("oscar", "grouch")
};
list l = arrays.aslist(n);
collections.sort(l);
system.out.println(l);
}
}
如果你运行这个程序,以下是它所打印的结果:
[oscar grouch, john lennon, groucho marx, karl marx]
对 compareto 方法的行为有四个限制,我们现在不作一一讨论,因为它们的技术性太强,并且十分枯燥,我们最好将其留在api文本中。但是,所有实现 comparable 的类都必须接受这些限制的约束,这一点是确实重要的。因此,如果你要编写一个实现comparable 的类,请读那些有关 comparable 的文本吧。要试图为违反了那些限制的对象的列表进行排序可能引发不可预知的行为。从技术上讲,这些限制保证了自然排序是实现它的类的对象的部分顺序(partial order)。保证排序被很好地定义是十分必要的。
比较器(comparators)
好,到目前为止,你已经了解了自然排序。那么,如果要对某些对象不按自然顺序进行排序,又会怎么样呢?或者,如果你要为某些不实现 comparable 的对象进行排序呢?为做这些事情,你需要提供一个comparator。 comparator 实际就是一个封装了排序的对象。与 comparable 接口类似,comparator 接口由一个的方法构成:
public interface comparator {
int compare(object o1, object o2);
}
compare 方法比较它的两个参数,当第一个参数小于、等于或大于第二个参数时,分别返回一个负整数、空或正整数。如果其中一个参数具有对 comparator 不适合的类型,compare 方法则扔出一个 classcastexception。
在上一节中的有关 comparable 的许多内容也适用comparator。编写一个 compare 方法几乎等同于编写一个compareto 方法,除前者是把两个参数都当作参数之外。compare 方法必须象comparable 的 compareto 方法一样,服从同样的四个"技术限制"。出于同样的原因, comparator 必须对它所比较的对象诱发一个 partial order(部分顺序)。
假设你有一个称作 employeerecord 的类:
public class employeerecord implements comparable {
public name name();
public int employeenumber();
public date hiredate();
...
}
我们假设 employeerecord 对象的自然排序是对雇员姓名的排序 (就象上一个例子中所定义的)。不幸的是,老板要求我们提出一个按雇员资历排序的列表。这就意味着我们必须做些额外的工作,但是不多。以下是一个将生成所需列表的程序:
import java.util.*;
class empsort {
static final comparator seniority_order = new comparator() {
public int compare(object o1, object o2) {
employeerecord r1 = (employeerecord) o1;
employeerecord r2 = (employeerecord) o2;
return r2.hiredate().compareto(r1.hiredate());
}
};
static final collection employees = ... ; // employee database
public static void main(string args[]) {
list emp = new arraylist(employees);
collections.sort(emp, seniority_order);
system.out.println(emp);
}
}
以上程序中的 comparator 相当简单。它将它的参数转换为employeerecord, 并依赖适用于 hiredate()方法的 date 的自然排序。请注意:comparator 将它的第二个参数的雇用-日期传递给第一个参数,而不是按反方向传递。 这是因为,最新雇用的雇员资历最浅:按照雇用-日期排序将使列表成为反向资历-顺序。另一个获得相同结果的方法是:保持参数顺序,但对比较结果求反。
return -r1.hiredate().compareto(r2.hiredate());
两种方法同样可取。使用哪一种,全由你自己。
以上程序中的 comparator ,在对 list 进行排序时,效果很好。但有一个小的缺陷:它不能被用来对一个排序的 对象集 (如treesetm) 进行排序,因为它生成一个严格的部分(strictly partial) 排序。这意味着这个comparator 使不相等的对象相等。特别的,它会使任意两个雇用日期相同的雇员成为相等。当你为一个 list 排序时,这没关系,但当你使用 comparator 为一个sort排序的对象集 排序时, 这就是致命的了。如果你将多个雇用日期相同的雇员用comparator插入到一个treeset之中,那么只有第一个将被添加到 set,第二个将被作为一个复制元素而忽略。
为解决这个问题,你必须做的一切就是修整 comparator 使之生成一个 total ordering(完整排序)。 换句话说,修整 comparator 是为了使在使用compare 时被认为相等的唯一元素即是那些在使用equals 时被认为相等的元素。 实现这个目的的途径是做一个两部分(two-part)比较 (就象我们为 name 做的那样),这里的第一部分是我们真正感兴趣的(此案例中为雇用-日期),而第二部分是可唯一识别的对象属性。在此案例中,雇员号是作为第二部分使用的明显的属性。请看下面的 comparator :
static final comparator seniority_order = new comparator() {
public int compare(object o1, object o2) {
employeerecord r1 = (employeerecord) o1;
employeerecord r2 = (employeerecord) o2;
int datecmp = r2.hiredate().compareto(r1.hiredate());
if (datecmp != 0)
return datecmp;
return (r1.employeenumber() $#@60; r2.employeenumber() ? -1 :
(r1.employeenumber() == r2.employeenumber() ? 0 : 1));
}
};
最后注意一点,你可能被引诱用更简单的程序来替代 comparator 中最后的 return 语句:
return r1.employeenumber() - r2.employeenumber();
不要这样做,除非你能绝对保证不出现一个负的雇员数!这个技巧不可普遍使用,因为一个带正负号的整数类型,即使再大,也不足以表示两个任意的带正负号的整数的差值。如果 i 是一个大的正整数,j 是一个大的负整数,i-j 将溢出并返回一个负整数。 comparator 的结果违反了我们一直在讲的四个技术限制之中的一个限制(传递性),并导致可怕而玄妙的故障。 这并不是一个纯技术问题;搞不好,它会伤着你。
sortedset 接口
sortedset是一个使其元素维持上升顺序的set, 它按照元素的自然顺序进行排序,或按照在sortedset 创建时提供的 comparator 进行排序(自然顺序和 comparators 已在前面的有关 object ordering 的章节中做过讨论)。除了正常的 set 操作之外, 该 set 接口还提供下列操作:
局域视图: 对 sorted set 执行任意局域操作。
端点:返回在 sorted set 中的第一个或最后一个元素。
比较器存取: 返回对 set 进行排序的comparator 。
sortedset 接口如下所示:
public interface sortedset extends set {
// range-view
sortedset subset(object fromelement, object toelement);
sortedset headset(object toelement);
sortedset tailset(object fromelement);
// endpoints
object first();
object last();
// comparator access
comparator comparator();
}
set 操作
从 set 继承的 sortedset 操作在 sorted sets 和正常 sets 上的表现完全相同,只有两个例外:
由 iterator 操作返回的 iterator 按顺序遍历 sorted set。
由 toarray 返回的数组按顺序包括 sorted set 的元素。
尽管该接口不保证这一点,但 jdk 的 sortedset 实现的 tostring 方法返回一个按顺序包含所有 sorted set 元素的串。
标准构造函数
按惯例,所有 collection 实现都提供一个采用一种 collection 的标准构造函数。sortedset 实现也不例外。该构造函数创建了一个sortedset 对象,它可按自然顺序为它的元素排序。除此之外,按惯例,sortedset 实现还提供另外两个标准构造函数:
一个构造函数采用 comparator 并返回一个新的(空的)按特定comparator 排序的 sortedset。
另一个构造函数采用 sortedset 并返回一个新的包含与给定 sortedset 相同的元素的 sortedset, 它按照相同的comparator进行排序 (或是用元素的自然顺序,如果给定的 sortedset 也这样做过的话)。 请注意,决定该构造函数是否比普通 set 构造函数可优先调用的是参数的编译时类型,而不是运行时类型!
第一个标准构造函数是用显式comparator 创建一个空的 sortedset 的一般方法。第二个标准构造函数在本质上与标准collection 构造函数相似:它用同样的排序创建一个 sortedset 的拷贝,但使用的是一个程序员指定的实现类型。
局域视图操作
这里的局域视图操作与 list 接口 提供的局域视图操作有些相似,但有一个大的区别。一个 sorted set 的局域视图将保持有效,即使后备 sorted set 被直接更改也不例外。这是可行的,因为一个 sorted set 的一个局域视图的端点是元素空间中的绝对点,而不是在后备 对象集 中的特定元素(如列表中的情况)。一个 sorted set 的局域视图实际恰恰是一个位于元素空间的指定部位的 set 的某个位置上的视窗。局域视图的变化写回到后备sorted set ,反之亦然。 因此,完全可以在 sorted sets 上长期使用局域视图 (与在列表上的局域视图不同)。
sorted sets 提供三个局域视图操作。第一个是subset,subset 采用两个端点 (就象 sublist中的操作一样)。该端点是对象,而不是索引。它们必须与 sorted set 中的元素是可比较的(使用 set 的 comparator 或它的元素的自然排序,只要是 set 用来为自己排序的那一个)。就象 sublist 一样,局域是半开放的(half-open), 它包括它的低端点,但不包括它的高端点。
于是,下面的一行程序将告知你在"doorbell" 和 "pickle"之间有多少个词(包括 "doorbell" 但不包括 "pickle")被包括在称作词典的串的 sortedset 之中:
int count = dictionary.subset("doorbell", "pickle").size();
类似的,下面的一行程序将删除所有以"f" 开始的元素(是不是一个很严厉的审查制度?):
dictionary.subset("f", "g").clear();
你可以使用相似的技巧打印表格,并告知你以每个字母开始的词有多少:
for (char ch="a"; ch$#@60;="z"; ch++) {
string from = new string(new char[] {ch});
string to = new string(new char[] {(char)(ch+1)});
system.out.println(from + ": " +
dictionary.subset(from, to).size());
}
假设你要视图一个封闭的区间(closed interval) (两个端点都被包括)而不是一个开放的区间。如果该元素类型允许对一个给定值进行后继符(successor) 计算(在该元素空间), 那么, subset 只要从 lowendpoint 至 successor(highendpoint) 发出请求。尽管这不是显而易见的,但是,在 string 的自然排序中的一个串 s 的后继符是 s+"/0" (即,s 加上一个空字符)。
于是,下面的一行程序将告知你在"doorbell" 和 "pickle"之间(包括 "doorbell" 和 "pickle")有多少词被包括在词典里:
dd$#@60;$#@62; int count = dictionary.subset("doorbell", "pickle/0").size();
类似的技术也可被用来视图一个开放的区间(open interval) (两个端点都不被包括)。从lowendpoint 至 highendpoint 的开放区间视图是从 successor(lowendpoint) 至 highendpoint 的半开放区间。下列程序计算在"doorbell" 和 "pickle"之间的词汇数(不包括上述两个词):
int count = dictionary.subset("doorbell/0", "pickle").size();
sortedset 接口还包括另外两个局域视图操作, headset 和 tailset, 这两个操作均采用一个单一的 object 参数。前者返回对后备 sortedset 的初始部分的一个视图,一直到该特定对象,但不包括该特定对象;后者返回这个后备sortedset 的最后部分的一个视图,它从这个特定对象开始,直到该后备 sortedset 的结束。于是,下列代码允许你把该词典作为分开的两"卷"来视图(a-m 和 n-z):
sortedset volume1 = dictionary.headset("n");
sortedset volume2 = dictionary.tailset("n");
端点操作
sortedset 接口返回 sorted set 中的第一个和最后一个元素的操作,称作 (不必惊讶) first 和 last。除了它们显而易见的用处外, last 还为 sortedset 接口中的缺陷准备了一个工作区。 你在 sortedset 上所要做的一件事情就是进入 set 的内部并向前或向后迭代。从内部向前迭代是非常容易的:只要采用tailset 并在它上面迭代就可以了。不幸的是,没有向后迭代的简单途径。
下列惯用程序可获取在一个 sorted set中的第一个元素,在元素空间中它小于一个特定对象 o :
object predecessor = ss.headset(o).last();
这是从一个 sorted set 内部的一点向后"走过"一个元素的好办法。它可重复地应用于向后迭代。但不幸的是,它的效率太低,它要求查找返回的每一个元素。
比较器存取操作(comparator accessor)
sortedset 接口包含一个被称作 comparator 的存取操作方法,它返回用来对 set 进行排序的 comparator, 如果该 set 是按照它的元素的自然顺序排序的,则返回 null 。提供这个方法的目的是为了使 sorted sets 能被拷贝为一个新的排序相同的 sorted sets 。它被 以上 描述的标准 sortedset 构造函数所采用。
sortedmap接口
sortedmap是一个将它的项保持为上升顺序的map, 它按照键的自然顺序进行排序,或按照在sortedmap 创建时提供的 comparator 进行排序(自然顺序和 comparators 已在前面的有关 object ordering 的章节中做过讨论)。除了正常的 map 操作之外, 该 map 接口还提供下列操作:
局域视图: 对 sorted map 执行任意局域操作。
端点:返回在 sorted map 中的第一个或最后一个键。
比较器存取: 返回对 map 进行排序的comparator 。 public interface sortedmap extends map { comparator comparator(); sortedmap submap(object fromkey, object tokey); sortedmap headmap(object tokey); sortedmap tailmap(object fromkey); object first(); object last(); } 这个接口是sortedset的 map等价物
map操作
sortedmap 从 map 继承的操作在 sorted maps 和正常 maps 上的行为是等同的,只有两个例外:
由对任意 sorted map 的 对象集视图的 iterator 操作所返回的 iterator 按顺序遍历对象集。
由 collection视图的 toarray 操作所返回的数组按顺序包括键、值或项。
尽管该接口不保证这一点,但在 jdk 的 sortedmap 实现中的collection视图的 tostring 方法返回一个按顺序包含视图的所有元素的串。
标准构造函数(standard constructors)
按惯例,所有的 map 实现都提供一个采用一个 map 的标准构造函数,sortedmap 实现也不例外。该构造函数创建了一个 sortedmap 对象,它按照它们的键的自然顺序对它的项进行排序。除此之外 ,按惯例,sortedmap 实现还提供另外两个标准构造函数:
一个构造函数采用一个 comparator 并返回一个新的(空的)按特定 comparator 排序的sortedmap。
另一个构造函数采用一个 sortedmap 并返回一个新的包含与给定的 sortedmap 的映射相同的 sortedmap,
它按照同样的 comparator进行排序 (或是用元素的自然顺序,如果特定的 sortedmap 也这样做过的话)。
请注意,决定该构造函数是否比普通 map 构造函数优先调用的是参数的编译时类型,而不是它的运行时类型! 第一个标准构造函数是用显式 comparator 创建一个空的 sortedset 的一般方法。第二个标准构造函数在本质上与标准 map 构造函数相似:它用同样的排序创建一个 sortedmap 的拷贝,但使用的是一个程序员指定的实现类型。
与sortedset的比较
因为这个接口是 sortedset 的一个精确的 map 对等物,所以,在 sortedset章节 中的所有的惯用程序和代码举例均适用于 sortedmap, 只需一些小的更改。
comparable 接口由一个单一的方法构成:
public interface comparable {
public int compareto(object o);
}
compareto 方法将接收对象与特定对象进行比较,并在接收对象小于、等于或大于特定对象时分别返回负整数、空或一个正整数。如果特定对象不能与接收对象相比较,该方法扔出一个classcastexception. 这是一个表示某人姓名的类(a class representing a person"s name), 它实现了 comparable:
import java.util.*;
public class name implements comparable {
private 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(object o) {
name n = (name)o;
int lastcmp = lastname.compareto(n.lastname);
return (lastcmp!=0 ? lastcmp :
firstname.compareto(n.firstname));
}
}
为了使这个例子短一些,该类受到了一点限制:它不支持中间名,它要求必须同时具有first name 和 last name, 而这不是在全世界都通用的。尽管如此,这个例子仍有几个重要之处:
name 对象是不变的( immutable)。作为相等、不变类型的所有其它事情就是如何做的问题,特别是对那些将被用来作为 sets 中的元素或 maps 中的键的对象来说,更是如此。如果你对这些 对象集 中的元素或键做了更改,这些 对象集 将中断。
构造函数可检查它的参数是否为 null。 这可以保证所有的name 对象都能很好地形成。因而没有其它方法会? nullpointerexception.
hashcode 方法被重新定义。对重新定义 equals 方法的任意类来说,这是必需的(essential)。 一般约定(general contract)需要 object.equals. (equal 对象必须具有相等的哈希代码) 。
如果特定对象为 null,或一个不适当的类型, equals 方法则返回 false。 在这种情况下, compareto 方法扔出一个运行时异常。这两个行为都是各自方法的一般约定所必需的。
tostring 方法已被重新定义,从而可以以人们能够读懂的形式打印 name 。这总是一个好主意,特别是对要被放入对象集 中的对象来说,更有益处。各种 对象集 类型的 tostring 方法依赖它们的元素、键和值的 tostring 方法。
由于这一节介绍的是有关元素排序的问题,因而让我们稍微多谈一点 name 的 compareto 方法。它实现标准的姓名-排序算法,在该算法中,last name 优先于 first name。这恰恰是你在一个natural ordering(自然排序)中所想要的。 如果自然排序不自然,那才容易引起混乱呢!
请看 compareto 是如何被实现的,因为它是相当典型的。首先,你将 object 参数转换为适当类型; 如果参数类型是不适当的,则会扔出一个适当的异常(classcastexception);那么你应该比较对象的最重要部分(在此案例中为 last name)。通常,你可以使用该部分的类型的自然排序。 在次案例中,该部分是一个 string, 并且自然的(按词典顺序的)排序正是所要求的。如果比较的结果是空(它表示等同性)之外的其它东西,你就做完了:你可以返回结果。 如果最重要的部分是相等的,你就应该继续比较次重要部分。在此案例中,只有两个部分 (first name and last name)。 如果有更多的部分,你就应该以显而易见的方式继续进行,直到发现两个不相等的部分(否则你就应该比较最不重要的部分),这时,你就可以返回比较结果了。这是 一个建立 name 对象列表并对它们进行排序的小程序:
import java.util.*;
class namesort {
public static void main(string args[]) {
name n[] = {
new name("john", "lennon"),
new name("karl", "marx"),
new name("groucho", "marx"),
new name("oscar", "grouch")
};
list l = arrays.aslist(n);
collections.sort(l);
system.out.println(l);
}
}
如果你运行这个程序,以下是它所打印的结果:
[oscar grouch, john lennon, groucho marx, karl marx]
对 compareto 方法的行为有四个限制,我们现在不作一一讨论,因为它们的技术性太强,并且十分枯燥,我们最好将其留在api文本中。但是,所有实现 comparable 的类都必须接受这些限制的约束,这一点是确实重要的。因此,如果你要编写一个实现comparable 的类,请读那些有关 comparable 的文本吧。要试图为违反了那些限制的对象的列表进行排序可能引发不可预知的行为。从技术上讲,这些限制保证了自然排序是实现它的类的对象的部分顺序(partial order)。保证排序被很好地定义是十分必要的。
比较器(comparators)
好,到目前为止,你已经了解了自然排序。那么,如果要对某些对象不按自然顺序进行排序,又会怎么样呢?或者,如果你要为某些不实现 comparable 的对象进行排序呢?为做这些事情,你需要提供一个comparator。 comparator 实际就是一个封装了排序的对象。与 comparable 接口类似,comparator 接口由一个的方法构成:
public interface comparator {
int compare(object o1, object o2);
}
compare 方法比较它的两个参数,当第一个参数小于、等于或大于第二个参数时,分别返回一个负整数、空或正整数。如果其中一个参数具有对 comparator 不适合的类型,compare 方法则扔出一个 classcastexception。
在上一节中的有关 comparable 的许多内容也适用comparator。编写一个 compare 方法几乎等同于编写一个compareto 方法,除前者是把两个参数都当作参数之外。compare 方法必须象comparable 的 compareto 方法一样,服从同样的四个"技术限制"。出于同样的原因, comparator 必须对它所比较的对象诱发一个 partial order(部分顺序)。
假设你有一个称作 employeerecord 的类:
public class employeerecord implements comparable {
public name name();
public int employeenumber();
public date hiredate();
...
}
我们假设 employeerecord 对象的自然排序是对雇员姓名的排序 (就象上一个例子中所定义的)。不幸的是,老板要求我们提出一个按雇员资历排序的列表。这就意味着我们必须做些额外的工作,但是不多。以下是一个将生成所需列表的程序:
import java.util.*;
class empsort {
static final comparator seniority_order = new comparator() {
public int compare(object o1, object o2) {
employeerecord r1 = (employeerecord) o1;
employeerecord r2 = (employeerecord) o2;
return r2.hiredate().compareto(r1.hiredate());
}
};
static final collection employees = ... ; // employee database
public static void main(string args[]) {
list emp = new arraylist(employees);
collections.sort(emp, seniority_order);
system.out.println(emp);
}
}
以上程序中的 comparator 相当简单。它将它的参数转换为employeerecord, 并依赖适用于 hiredate()方法的 date 的自然排序。请注意:comparator 将它的第二个参数的雇用-日期传递给第一个参数,而不是按反方向传递。 这是因为,最新雇用的雇员资历最浅:按照雇用-日期排序将使列表成为反向资历-顺序。另一个获得相同结果的方法是:保持参数顺序,但对比较结果求反。
return -r1.hiredate().compareto(r2.hiredate());
两种方法同样可取。使用哪一种,全由你自己。
以上程序中的 comparator ,在对 list 进行排序时,效果很好。但有一个小的缺陷:它不能被用来对一个排序的 对象集 (如treesetm) 进行排序,因为它生成一个严格的部分(strictly partial) 排序。这意味着这个comparator 使不相等的对象相等。特别的,它会使任意两个雇用日期相同的雇员成为相等。当你为一个 list 排序时,这没关系,但当你使用 comparator 为一个sort排序的对象集 排序时, 这就是致命的了。如果你将多个雇用日期相同的雇员用comparator插入到一个treeset之中,那么只有第一个将被添加到 set,第二个将被作为一个复制元素而忽略。
为解决这个问题,你必须做的一切就是修整 comparator 使之生成一个 total ordering(完整排序)。 换句话说,修整 comparator 是为了使在使用compare 时被认为相等的唯一元素即是那些在使用equals 时被认为相等的元素。 实现这个目的的途径是做一个两部分(two-part)比较 (就象我们为 name 做的那样),这里的第一部分是我们真正感兴趣的(此案例中为雇用-日期),而第二部分是可唯一识别的对象属性。在此案例中,雇员号是作为第二部分使用的明显的属性。请看下面的 comparator :
static final comparator seniority_order = new comparator() {
public int compare(object o1, object o2) {
employeerecord r1 = (employeerecord) o1;
employeerecord r2 = (employeerecord) o2;
int datecmp = r2.hiredate().compareto(r1.hiredate());
if (datecmp != 0)
return datecmp;
return (r1.employeenumber() $#@60; r2.employeenumber() ? -1 :
(r1.employeenumber() == r2.employeenumber() ? 0 : 1));
}
};
最后注意一点,你可能被引诱用更简单的程序来替代 comparator 中最后的 return 语句:
return r1.employeenumber() - r2.employeenumber();
不要这样做,除非你能绝对保证不出现一个负的雇员数!这个技巧不可普遍使用,因为一个带正负号的整数类型,即使再大,也不足以表示两个任意的带正负号的整数的差值。如果 i 是一个大的正整数,j 是一个大的负整数,i-j 将溢出并返回一个负整数。 comparator 的结果违反了我们一直在讲的四个技术限制之中的一个限制(传递性),并导致可怕而玄妙的故障。 这并不是一个纯技术问题;搞不好,它会伤着你。
sortedset 接口
sortedset是一个使其元素维持上升顺序的set, 它按照元素的自然顺序进行排序,或按照在sortedset 创建时提供的 comparator 进行排序(自然顺序和 comparators 已在前面的有关 object ordering 的章节中做过讨论)。除了正常的 set 操作之外, 该 set 接口还提供下列操作:
局域视图: 对 sorted set 执行任意局域操作。
端点:返回在 sorted set 中的第一个或最后一个元素。
比较器存取: 返回对 set 进行排序的comparator 。
sortedset 接口如下所示:
public interface sortedset extends set {
// range-view
sortedset subset(object fromelement, object toelement);
sortedset headset(object toelement);
sortedset tailset(object fromelement);
// endpoints
object first();
object last();
// comparator access
comparator comparator();
}
set 操作
从 set 继承的 sortedset 操作在 sorted sets 和正常 sets 上的表现完全相同,只有两个例外:
由 iterator 操作返回的 iterator 按顺序遍历 sorted set。
由 toarray 返回的数组按顺序包括 sorted set 的元素。
尽管该接口不保证这一点,但 jdk 的 sortedset 实现的 tostring 方法返回一个按顺序包含所有 sorted set 元素的串。
标准构造函数
按惯例,所有 collection 实现都提供一个采用一种 collection 的标准构造函数。sortedset 实现也不例外。该构造函数创建了一个sortedset 对象,它可按自然顺序为它的元素排序。除此之外,按惯例,sortedset 实现还提供另外两个标准构造函数:
一个构造函数采用 comparator 并返回一个新的(空的)按特定comparator 排序的 sortedset。
另一个构造函数采用 sortedset 并返回一个新的包含与给定 sortedset 相同的元素的 sortedset, 它按照相同的comparator进行排序 (或是用元素的自然顺序,如果给定的 sortedset 也这样做过的话)。 请注意,决定该构造函数是否比普通 set 构造函数可优先调用的是参数的编译时类型,而不是运行时类型!
第一个标准构造函数是用显式comparator 创建一个空的 sortedset 的一般方法。第二个标准构造函数在本质上与标准collection 构造函数相似:它用同样的排序创建一个 sortedset 的拷贝,但使用的是一个程序员指定的实现类型。
局域视图操作
这里的局域视图操作与 list 接口 提供的局域视图操作有些相似,但有一个大的区别。一个 sorted set 的局域视图将保持有效,即使后备 sorted set 被直接更改也不例外。这是可行的,因为一个 sorted set 的一个局域视图的端点是元素空间中的绝对点,而不是在后备 对象集 中的特定元素(如列表中的情况)。一个 sorted set 的局域视图实际恰恰是一个位于元素空间的指定部位的 set 的某个位置上的视窗。局域视图的变化写回到后备sorted set ,反之亦然。 因此,完全可以在 sorted sets 上长期使用局域视图 (与在列表上的局域视图不同)。
sorted sets 提供三个局域视图操作。第一个是subset,subset 采用两个端点 (就象 sublist中的操作一样)。该端点是对象,而不是索引。它们必须与 sorted set 中的元素是可比较的(使用 set 的 comparator 或它的元素的自然排序,只要是 set 用来为自己排序的那一个)。就象 sublist 一样,局域是半开放的(half-open), 它包括它的低端点,但不包括它的高端点。
于是,下面的一行程序将告知你在"doorbell" 和 "pickle"之间有多少个词(包括 "doorbell" 但不包括 "pickle")被包括在称作词典的串的 sortedset 之中:
int count = dictionary.subset("doorbell", "pickle").size();
类似的,下面的一行程序将删除所有以"f" 开始的元素(是不是一个很严厉的审查制度?):
dictionary.subset("f", "g").clear();
你可以使用相似的技巧打印表格,并告知你以每个字母开始的词有多少:
for (char ch="a"; ch$#@60;="z"; ch++) {
string from = new string(new char[] {ch});
string to = new string(new char[] {(char)(ch+1)});
system.out.println(from + ": " +
dictionary.subset(from, to).size());
}
假设你要视图一个封闭的区间(closed interval) (两个端点都被包括)而不是一个开放的区间。如果该元素类型允许对一个给定值进行后继符(successor) 计算(在该元素空间), 那么, subset 只要从 lowendpoint 至 successor(highendpoint) 发出请求。尽管这不是显而易见的,但是,在 string 的自然排序中的一个串 s 的后继符是 s+"/0" (即,s 加上一个空字符)。
于是,下面的一行程序将告知你在"doorbell" 和 "pickle"之间(包括 "doorbell" 和 "pickle")有多少词被包括在词典里:
dd$#@60;$#@62; int count = dictionary.subset("doorbell", "pickle/0").size();
类似的技术也可被用来视图一个开放的区间(open interval) (两个端点都不被包括)。从lowendpoint 至 highendpoint 的开放区间视图是从 successor(lowendpoint) 至 highendpoint 的半开放区间。下列程序计算在"doorbell" 和 "pickle"之间的词汇数(不包括上述两个词):
int count = dictionary.subset("doorbell/0", "pickle").size();
sortedset 接口还包括另外两个局域视图操作, headset 和 tailset, 这两个操作均采用一个单一的 object 参数。前者返回对后备 sortedset 的初始部分的一个视图,一直到该特定对象,但不包括该特定对象;后者返回这个后备sortedset 的最后部分的一个视图,它从这个特定对象开始,直到该后备 sortedset 的结束。于是,下列代码允许你把该词典作为分开的两"卷"来视图(a-m 和 n-z):
sortedset volume1 = dictionary.headset("n");
sortedset volume2 = dictionary.tailset("n");
端点操作
sortedset 接口返回 sorted set 中的第一个和最后一个元素的操作,称作 (不必惊讶) first 和 last。除了它们显而易见的用处外, last 还为 sortedset 接口中的缺陷准备了一个工作区。 你在 sortedset 上所要做的一件事情就是进入 set 的内部并向前或向后迭代。从内部向前迭代是非常容易的:只要采用tailset 并在它上面迭代就可以了。不幸的是,没有向后迭代的简单途径。
下列惯用程序可获取在一个 sorted set中的第一个元素,在元素空间中它小于一个特定对象 o :
object predecessor = ss.headset(o).last();
这是从一个 sorted set 内部的一点向后"走过"一个元素的好办法。它可重复地应用于向后迭代。但不幸的是,它的效率太低,它要求查找返回的每一个元素。
比较器存取操作(comparator accessor)
sortedset 接口包含一个被称作 comparator 的存取操作方法,它返回用来对 set 进行排序的 comparator, 如果该 set 是按照它的元素的自然顺序排序的,则返回 null 。提供这个方法的目的是为了使 sorted sets 能被拷贝为一个新的排序相同的 sorted sets 。它被 以上 描述的标准 sortedset 构造函数所采用。
sortedmap接口
sortedmap是一个将它的项保持为上升顺序的map, 它按照键的自然顺序进行排序,或按照在sortedmap 创建时提供的 comparator 进行排序(自然顺序和 comparators 已在前面的有关 object ordering 的章节中做过讨论)。除了正常的 map 操作之外, 该 map 接口还提供下列操作:
局域视图: 对 sorted map 执行任意局域操作。
端点:返回在 sorted map 中的第一个或最后一个键。
比较器存取: 返回对 map 进行排序的comparator 。 public interface sortedmap extends map { comparator comparator(); sortedmap submap(object fromkey, object tokey); sortedmap headmap(object tokey); sortedmap tailmap(object fromkey); object first(); object last(); } 这个接口是sortedset的 map等价物
map操作
sortedmap 从 map 继承的操作在 sorted maps 和正常 maps 上的行为是等同的,只有两个例外:
由对任意 sorted map 的 对象集视图的 iterator 操作所返回的 iterator 按顺序遍历对象集。
由 collection视图的 toarray 操作所返回的数组按顺序包括键、值或项。
尽管该接口不保证这一点,但在 jdk 的 sortedmap 实现中的collection视图的 tostring 方法返回一个按顺序包含视图的所有元素的串。
标准构造函数(standard constructors)
按惯例,所有的 map 实现都提供一个采用一个 map 的标准构造函数,sortedmap 实现也不例外。该构造函数创建了一个 sortedmap 对象,它按照它们的键的自然顺序对它的项进行排序。除此之外 ,按惯例,sortedmap 实现还提供另外两个标准构造函数:
一个构造函数采用一个 comparator 并返回一个新的(空的)按特定 comparator 排序的sortedmap。
另一个构造函数采用一个 sortedmap 并返回一个新的包含与给定的 sortedmap 的映射相同的 sortedmap,
它按照同样的 comparator进行排序 (或是用元素的自然顺序,如果特定的 sortedmap 也这样做过的话)。
请注意,决定该构造函数是否比普通 map 构造函数优先调用的是参数的编译时类型,而不是它的运行时类型! 第一个标准构造函数是用显式 comparator 创建一个空的 sortedset 的一般方法。第二个标准构造函数在本质上与标准 map 构造函数相似:它用同样的排序创建一个 sortedmap 的拷贝,但使用的是一个程序员指定的实现类型。
与sortedset的比较
因为这个接口是 sortedset 的一个精确的 map 对等物,所以,在 sortedset章节 中的所有的惯用程序和代码举例均适用于 sortedmap, 只需一些小的更改。
闽公网安备 35060202000074号