【笔记14】考虑实现Comparable接口

1、Comparable接口简介

    compareTo方法并没有在Object中声明。相反,它是Comparable接口中唯一的一个方法。compareTo方法不但允许进行简单的同等性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特性,还是个涉及到泛型的方法。类实现了Comparable接口,就表明它的实例具有内在的排序关系(natural ordering)。给实现Comparable接口的对象数组进行排序,只需要下面这一行代码:

Arrays.sort(a);

    对于存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也很简单【Java平台类库中的所有值类(value classes)都实现了Comparable接口】。如下按字母表顺序打印:

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

Comparable接口使用规范

    compareTo方法的通用约定和equals方法的相似,将一个对象与指定对象进行比较。当该对象小于、等于或者大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法和该对象进行比较,则抛出ClassCastException异常。

     在下面的说明中,符号sgn(表达式)表示数学中的signum函数,它根据表达式(expression)的值为负数、零和整数,分别返回-1、0或者1。

  • 必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。这也暗示着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才抛出异常。这条规则和equals使用规范里面的对称性类似。
  • 必须确保这个比较关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)暗示着x.compareTo(z) > 0也成立。对应着equals使用规范里面的传递性。
  • 必须确保x.compareTo(y) == 0暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
  • 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但是这个并非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应该明确予以说明。推荐使用这样的说法:“注意,该类具有内在的排序功能,但是与equals不一致”。

    与equals不同的是,在跨越不同类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同类的对象,compareTo可以抛出ClassCastException异常。

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

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

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

    CompareTo方法中域的比较是顺序地比较,而不是等同性的比较。比较对象引用域可以是通过递归地调用compareTo方法来实现。如果一个域并没有实现Comparable接口,或者你需要Comparator,或者使用已有的Comparator。例如针对下面的这个类,已经有一个compareTo方法:

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    public int comparaTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
    }

    ... //Remainder omitted
}

    比较整数型基本类型的域,可以使用关系操作符 < 和 > 。例如,浮点域用Double.compare或者Float.compare,而不用关系操作符,当应用到浮点值时,他们没有遵守compareTo的通用约定。对于数组域,则要把这些指导原则应用到每个元素上。

    如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。你必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果(零代表相等),则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则进一步比较次关键的域,以此类推。如果所有的域都是相等的,则对象就是相等的,并返回零。例如下面的例子:

public final class PhoneNumber implements Comparable {

    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix,
                        int lineNumber) {
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    @Override
    public int compareTo(PhoneNumber pn) {
        if (areaCode < pn.areaCode) 
            return -1;
        if(areaCode > pn.areaCode)
            return 1;

        if (prefix < pn.prefix)
            return -1;
        if (prefix > pn.prefix)
            return 1;

        if (lineNumber < pn.lineNumber)
            return -1;
        if (lineNumber > pn.lineNumber)
            return 1;

        return 0;
    }
}

    虽然这个方法可行,但它还可以进行改进。先来看看compareTo方法的约定,并没有指定返回值的大小(magnitude),而只是指定了返回值的符号。可以利用这一点来简化代码,或许还可以提高它的运行速度:

public int compareTo(PhoneNumber pn) {
    int areaCodeDiff = areaCode - pn.areaCode;
    if (areaCodeDiff != 0)
        return areaCodeDiff;

    int prefixDiff = prefix - pn.prefix;
    if (0 != prefixDiff)
        return prefixDiff;

    return lineNumber - pn.lineNumber;
}

    使用这种方法的时候需要注意:最小和最大的可能域值之差小于或等于INTEGER.MAX_VALUE(2^31 -1),否则最好不要使用。这种技巧有时候不能正常工作的原因在于:有符号的32位整数还不足以大到能够表达任意两个32位整数的差值,如果i是一个很大的正整数,j是一个很小的负整数,i-j有可能会溢出,并且返回一个负值,这就违反了compareTo约定的第一条和第二条,导致失败。

踩坑指南

    如下code,compareTo方法报错llegalArgumentException:

// 错误的方式  
Collections.sort(list, new Comparator<Integer>() {  
    @Override  
    public int compare(Integer int1, Integer int2) {  
        return int1> int2? 1 : -1;
    }  
});

当int1 = int2时,违背规则:必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。对x = y的情况要做额外的细致处理!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值