深入领略Collections API
副标题#e#
一个 List l 大概被做如下排序:
Collections.sort(l);
假如这个 list 由 String 元素所构成, 那么它将按辞书排序法(按字母顺序)举办排序; 假如它是由 Date 元素所构成, 那么它将按年月顺序来排序。Java 怎么会知道该怎么做呢? 这必然是个把戏! 其实否则。实际上, String 和 Date 均实现了Comparable接口。Comparable 接口为一个类提供一个自然排序( natural ordering), 它答允谁人类的工具被自动排序。下表列出了实现了Comparable 的JDK类:
类 自然排序
Byte 带标记的数字排序
Character 不带标记的数字排序
Long 带标记的数字排序
Integer 带标记的数字排序
Short 带标记的数字排序
Double 带标记的数字排序
Float 带标记的数字排序
BigInteger 带标记的数字排序
BigDecimal 带标记的数字排序
File 依赖系统的按路径名字母顺序排序
String 按字母顺序排序
Date 按年月顺序排序
CollationKey 特定字符集按字母顺序排序
假如你要为一个其元素没有实现 Comparable的列表排序,Collections.sort(list) 将扔出一个 ClassCastException。雷同的,假如你要为一个其元素没有作彼此较量的列表举办排序, Collections.sort 将扔出一个 ClassCastException. 可以或许被彼此较量的元素被称作 mutually comparable(可彼此较量的)。固然差异范例的元素有大概被彼此较量,但以上列出的任何JDK范例都不答允在类之间的较量 (inter-class comparison)。
假如你只是要为可较量的元素的列表举办排序,或为它们建设排序的工具集, 则这就是你实际需要相识的全部有关 Comparable 接口的内容。假如你要实现你本身的 Comparable 范例,则下一节将会引起你的乐趣。
编写你本身的 Comparable 范例
Comparable 接口由一个单一的要领组成:
public interface Comparable {
public int compareTo(Object o);
}
#p#副标题#e#
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(自然排序)中所想要的。假如自然排序不自然,那才容易引起杂乱呢!
#p#分页标题#e#
请看 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());
两种要领同样可取。利用哪一种,全由你本身。
#p#分页标题#e#
以上措施中的 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() < r2.employeeNumber() ? -1 : (r1.employeeNumber() == r2.employeeNumber() ? 0 : 1)); }};
最后留意一点,你大概被引诱用更简朴的措施来替代 Comparator 中最后的 return 语句:
return r1.employeeNumber() – r2.employeeNumber();
不要这样做,除非你能绝对担保不呈现一个负的雇员数!这个能力不行普遍利用,因为一个带正负号的整数范例,纵然再大,也不敷以暗示两个任意的带正负号的整数的差值。假如 i 是一个大的正整数,j 是一个大的负整数,i-j 将溢出并返回一个负整数。Comparator 的功效违反了我们一直在讲的四个技能限制之中的一个限制(通报性),并导致可骇而奥妙的妨碍。这并不是一个纯技能问题;搞欠好,它会伤着你。