第12条:考虑实现Comparable接口

第12条:考虑实现Comparable接口


Comparable接口简介

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

Arrays.sort(a);

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

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

    一旦类实现了Comparable接口,它就可以跟许多泛型算法(generic algorithm)以及依赖于该接口的集合实现(collection implementation)进行协作。你付出了很小的努力就可以获得非常强大的功能。事实上,Java平台类库中的所有值类(value classes)都实现了Comparable接口。如果你正在编写一个值类,它具有非常明显的内在排序关系,那就应该优先考虑实现这个接口:

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

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不一致”。

    在类的内部,任何合理的属虚关系都可以满足compareTo的约定。在跨越不同类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同类的对象,compareTo可以抛出ClassCastException异常。通常,这正是compareTo在这种情况下应该做的事情,如果类设置了确定的参数,这也正式它要做的事情。虽然以上约定没有把跨类之间的比较排除在外,但是从Java1.6发行版本开始,Java平台类库中就没有支持跨类比较的这种特性了。

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

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

    compareTo约定的最后一段是一个强烈的建议,而不是真正的规则,只是说明了compareTo方法施加的同等性测试,在通常情况下就应该返回与equals方法同样的结果。如果遵守了这一条规定,那么由compareTo方法所施加的关系顺序就会被认为”于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方法进行比较的时候是相等的。

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

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

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

    ... //Remainder omitted
}

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

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

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;
}

    使用这种方法的时候需要注意,有符号的32位整数还不足以大到能够表达任意两个32位整数的差值,如果i是一个很大的正整数,j是一个很小的负整数,i-j有可能会溢出,并且返回一个负值= =,正如你所看到的那样,黑白直接被颠倒过来了。

踩过的一个坑

    在写java的时候,有用到过Comparable接口,当时用的是jdk7,但是习惯上还是用jdk6的惯例来写,对于compareTo方法,我开始是这样写的,不知道为什么经常报错llegalArgumentException:

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

    这样写大致一看没什么错误诶,之前在jdk6上也可以跑的很好,以前写都可以通过,这次尼玛怎么也通不过了。后来在网上找了好久的资料,有说是jdk7和jdk6的不兼容= =,于是看了看,

Area: API: Utilities  
Synopsis: Updated sort behavior for Arrays and Collections may throw an IllegalArgumentException  
Description: The sorting algorithm used by java.util.Arrays.sort and (indirectly) by java.util.Collections.sort has been replaced.   
The new sort implementation may throw an IllegalArgumentException if it detects a Comparable that violates the Comparable contract.   
The previous implementation silently ignored such a situation.  
If the previous behavior is desired, you can use the new system property, java.util.Arrays.useLegacyMergeSort,   
to restore previous mergesort behavior.  
Nature of Incompatibility: behavioral  
RFE: 6804124

    官方说道,Arrays和Collections可能会抛出IllegalArgumentException的一场,因为Arrays.sort和Collections.sort这两种算法已经被新的算法取代了。如果实现的compareTo方法违反了相应的规范,就会抛出一场,但是jdk6也就是之前的版本直接忽略了这个问题。下面是compareTo新的算法说明:

int compare(T o1,
      T o2)
Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
In the foregoing description, the notation sgn(expression) designates the mathematical signum function, which is defined to return one of -1, 0, or 1 according to whether the value of expression is negative, zero or positive.

The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and y. (This implies that compare(x, y) must throw an exception if and only if compare(y, x) throws an exception.)

The implementor must also ensure that the relation is transitive: ((compare(x, y)>0) && (compare(y, z)>0)) implies compare(x, z)>0.

Finally, the implementor must ensure that compare(x, y)==0 implies that sgn(compare(x, z))==sgn(compare(y, z)) for all z.

It is generally the case, but not strictly required that (compare(x, y)==0) == (x.equals(y)). Generally speaking, any comparator that violates this condition should clearly indicate this fact. The recommended language is "Note: this comparator imposes orderings that are inconsistent with equals."

    在里面提到了这么一句话,是之前这个方法所违背的,

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

    里面有这么一句话:The implementor must ensure that sgn(compare(x, y)) == -sgn(compare(y, x)) for all x and y.,但是当x == y的时候,这条规则并不成立。因此需要对x == y进行额外的处理。以后在用的时候要更加的小心。

你可能感兴趣的:(Effective,Java)