考虑实现Comparable接口

考虑实现Comparable接口
与本章中讨论的其他方法不同,compareTo方法并没有在Object 类中声明。相反,它是Comparable 接口中唯一的方法。compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特征,它还是个泛型(generic)。类实现了 Comparable接口,就表明它的实例具有内在的排序关系 (natural ordering)。为实现Comparable接口的对象数组进行排序就这么简单:

Arrays.sort(a);

对存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也同样简单。例如,下面的程序依赖于实现了 Comparable接口的String类,它去掉了命令行参数列表中的重复参数,并按字母顺序打印出来:

public class WordList {
public static void main(String[] args) {
Set s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}

通过实现 Comparable 接口,可以让你的类与所有依赖此接口的通用算法和集合实现进行互操作。只需少量的努力就可以获得非常强大的功能。几乎 Java 平台类库中的所有值类以及所有枚举类型(详见第 34 条)都实现了 Comparable 接口。如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现 Comparable 接口:

public interface Comparable {
int compareTo(T t);
}

compareTo 方法的通用约定与 equals 相似:

将这个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候, 分别返回一个负整数、零或者正整数。如果指定对象的类型与此对象不能进行比较,则引发ClassCastException 异常。

下面的描述中,符号 sgn(expression) 表示数学中的 signum 函数,它根据表达式的值为负数、零、正数,对应返回-1、0 和 1。

实现类必须确保所有 x 和 y 都满足 sgn(x.compareTo(y)) == -sgn(y. compareTo(x)) 。(这意味着当且仅当 y.compareTo(x) 抛出异常时, x.compareTo(y) 必须抛出异常。)
实现类还必须确保该关系是可传递的:(x. compareTo(y) > 0 && y.compareTo(z) > 0) 意味着x.compareTo(z) > 0 。
最后,实现者必须确保 x. compareTo ( y) == 0 暗示着所有的 z 都满足 sgn(xcompareTo ( z) ) == sgn ( y . compare To ( z))。
强烈推荐 (x.compareTo(y) == 0) == (x.equals(y)) ,但不是必需的。一般来说,任何实现了Comparable 接口的类违反了这个条件都应该清楚地说明这个事实。如:注意:该类具有内在的排序功能,但是与 equals 不一致。
千万不要被上述约定中的数学关系所迷惑。如同equals约定(详见第10条)一样,compareTo约定并没有看起来那么复杂。与equals方法不同的是,它对所有的对象强行施加了一种通用的等同关系,compareTo不能跨越不同类型的对象进行比较:在比较不同类型的对象时,compareTo可以抛出ClassCastException异常。通常,这正是 compareTo在这种情况下应该做的事情。合约确实允许进行跨类型之间的比较,这一般是在被比较对象实现的接口中进行定义。

就好像违反了hashCode约定的类会破坏其他依赖于散列的类一样,违反compareTo约定的类也会破坏其他依赖于比较关系的类。依赖于比较关系的类包括有序集合类TreeSet和TreeMap,以及工具类Collections和Arrays,它们内部包含有搜索和排序算法。

现在我们来回顾一下compareTo约定中的条款。第一条指出,如果颠倒了两个对象引用之间的比较方向,就会发生下面的情况:如果第一个对象小于第二个对象,则第二个对象一定大于第一个对象;如果第一个对象等于第二个对象,则第二个对象一定等于第一个对象;如果第一个对象大于第二个对象,则第二个对象一定小于第一个对象。第二条指出,如果一个对象大于第二个对象,并且第二个对象又大于第三个对象,那么第一个对象一定大于第三个对象。最后一条指出,在比较时被认为相等的所有对象,它们跟别的对象做比较时一定会产生同样的结果。

这三个条款的一个直接结果是,由compareTo方法施加的等同性测试,也必须遵守相同于equals约定所施加的限制条件:自反性、对称性和传递性。因此,下面的告诫也同样适用:无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,除非愿意放弃面向对象的抽象优势(详见第10条)。针对equals的权宜之计也同样适用于compareTo方法。如果你想为一个实现了Comparable接口的类增加值组件,请不要扩展这个类;而是要编写一个不相关的类,其中包含第一个类的一个实例。然后提供一个“视图”(view)方法返回这个实例。这样既可以让你自由地在第二个类上实现compareTo方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例。

compareTo约定的最后一段是一条强烈的建议,而不是真正的规则,它只是说明了compareTo方法施加的等同性测试,在通常情况下应该返回与equals方法同样的结果。如果遵守了这一条,那么由compareTo方法所施加的顺序关系就被认为与equals一致。如果违反了这条规则,顺序关系就被认为与equals不一致。如果一个类的compareTo方法施加了一个与equals方法不一致的顺序关系,它仍然能够正常工作,但是如果一个有序集合(sorted collection)包含了该类的元素,这个集合就可能无法遵守相应集合接口(Collection、Set或Map)的通用约定。因为对于这些接口的通用约定是按照equals 方法来定义的,但是有序集合使用了由compareTo方法而不是equals方法所施加的等同性测试。尽管岀现这种情况不会造成灾难性的后果,但是应该有所了解。

例如,以BigDecimal类为例,它的compareTo方法与equals不一致。如果你创建了一个空的 HashSet实例,并且添加 new BigDecimal(“1.0”)和 new BigDecimal(“1.00”),这个集合就将包含两个元素,因为新增到集合中的两个BigDecimal实例,通过equals方法来比较时是不相等的。然而,如果你使用TreeSet而不是HashSet来执行同样的过程,集合中将只包含一个元素,因为这两个BigDecimal实例在通过compareTo方法进行比较时是相等的。(详情请参阅BigDecimal的文档。)

编写compareTo方法与编写equals方法非常相似,但也存在几处重大的差别。因为Comparable接口是参数化的,而且comparable方法是静态的类型,因此不必进行类型检查,也不必对它的参数进行类型转换。如果参数的类型不合适,这个调用甚至无法编译。如果参数为null,这个调用应该抛出NullPointerException异常,并且一旦该方法试图访问它的成员时就应该抛出异常。

CompareTo方法中域的比较是顺序的比较,而不是等同性的比较。比较对象引用域可以通过递归地调用compareTo方法来实现。如果一个域并没有实现Comparable接口,或者你需要使用一个非标准的排序关系,就可以使用一个显式的Comparator来代替。或者编写自己的比较器,或者使用已有的比较器,例如针对第10条中的CaseinsensitiveString 类的这个compareTo方法使用一个已有的比较器:

// Single-field Comparable with object reference field
public final class CaseInsensitiveString
implements Comparable {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
… // Remainder omitted
}

请注意, CaseInsensitiveString 类实现了 Comparable 接口。这意味着 CaseInsensitiveString 引用只能与另一个 CaseInsensitiveString 引用进行比较。

如果一个类有多个重要的属性,那么比较他们的顺序是至关重要的。从最重要的属性开始,逐步比较所有的重要属性。如果比较结果不是零(零表示相等),则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则进一步比 较次关键的域,以此类推。如果所有的域都是相等的,则对象就是相等的,并返回零。下面 通过第11条中的PhoneNumber类的compareTo方法来说明这种方法:

// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
int result = Short.compare(areaCode, pn.areaCode);
if (result == 0) {
result = Short.compare(prefix, pn.prefix);
if (result == 0)
result = Short.compare(lineNum, pn.lineNum);
}
return result;
}

在 Java 8 中 Comparator 接口提供了一系列比较器方法,可以使比较器流畅地构建。这些比较器可以用来实现 compareTo 方法,就像 Comparable 接口所要求的那样。许多程序员更喜欢这种方法的简洁性,尽管它的性能并不出众:在我的机器上排序 PhoneNumber 实例的数组速度慢了大约10%。在使用这种方法时,考虑使用 Java 的静态导入,以便可以通过其简单名称来引用比较器静态方法,以使其清晰简洁。以下是 PhoneNumber 的 compareTo 方法的使用方法:

// Comparable with comparator construction methods
private static final Comparator COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}

此实现在类初始化时构建比较器,使用两个比较器构建方法。第一个是 comparingInt 方法。它是一个静态方法,它使用一个键提取器函数式接口(key extractor function)作为参数,将对象引用映射为 int 类型的键,并返回一个根据该键排序的实例的比较器。在前面的示例中, comparingInt 方法使用lambda表达式,它从PhoneNumber中提取区域代码,并返回一个Comparator ,根据它们的区域代码来排序电话号码。注意,lambda 表达式显式指定了其输入参数的类型 (PhoneNumber pn)。事实证明,在这种情况下,Java 的类型推断功能不够强大,无法自行判断类型,因此我们不得不帮助它以使程序编译。

如果两个电话号码实例具有相同的区号,则需要进一步细化比较,这正是第二个比较器构建方法,即 thenComparingInt 方法做的。它是 Comparator 上的一个实例方法,接受一个 int 类型键提取器函数式接口(key extractor function)作为参数,并返回一个比较器,该比较器首先应用原始比较器,然后使用提取的键来打破连接。你可以按照喜欢的方式多次调用 thenComparingInt 方法,从而产生一个字典顺序。在上面的例子中,我们将两个调用叠加到 thenComparingInt ,产生一个排序,它的二级键是 prefix,而其三级键是 lineNum。请注意,我们不必指定传递给 thenComparingInt 的任何一个调用的键提取器函数式接口的参数类型:Java 的类型推断足够聪明,可以自己推断出参数的类型。

Comparator 类具有完整的构建方法。对于long和double 基本类型,也有对应的类似于comparingInt 和 thenComparingInt 的方法,int 版本的方法也可以应用于取值范围小于 int 的类型上,如 short 类型,如 PhoneNumber 实例中所示。对于 double 版本的方法也可以用在 float 类型上。这提供了所有 Java 的基本数字类型的覆盖。

也有对象引用类型的比较器构建方法。静态方法 comparing 有两个重载方式。第一个方法使用键提取器函数式接口并按键的自然顺序。第二种方法是键提取器函数式接口和比较器,用于键的排序。thenComparing 方法有三种重载。第一个重载只需要一个比较器,并使用它来提供一个二级排序。第二次重载只需要一个键提取器函数式接口,并使用键的自然顺序作为二级排序。最后的重载方法同时使用一个键提取器函数式接口和一个比较器来用在提取的键上。

有时,你可能会看到 compareTo 或 compare 方法依赖于两个值之间的差值,如果第一个值小于第二个值,则为负;如果两个值相等则为零,如果第一个值大于,则为正值。这是一个例子:

// BROKEN difference-based comparator - violates transitivity!
static Comparator hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};

千万不要使用这个方法 它很容易造成整数溢出,同时违反 IEEE 754 浮点算术标准[ JLS 15.20.1, 15.21.1 ]甚至,与利用本条目讲到的方法编写的那些方法相比,最终得到的方法并没有明显变快 因此,要么使用 一个静态方法 compare:

// Comparator based on static compare method
static Comparator hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};

或者使用 Comparator 的构建方法:

// Comparator based on Comparator construction method
static Comparator hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());

总而言之,每当实现一个对排序敏感的类时,都应该让这个类实现Comparable接 口,以便其实例可以轻松地被分类、搜索,以及用在基于比较的集合中。每当在compareTo方法的实现中比较域值时,都要避免使用 < 和 > 操作符,而应该在装箱基本类型的类中使用静态的compare方法,或者在Comparator接口中使用比较器构造方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值