第12条:考虑实现Comparable接口

术语:



        CompareTo方法并没有在Object方法中声名,而且他是Comparable接口中唯一的方法,compareTo不但允许进行简单的相等性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特征。他是个泛型,在文档中对其描述如下:

public interface Comparable<T>

This interface imposes a total ordering on the objects of each class that implements it. This ordering is referred to as the class's natural ordering, and the class's compareTo method is referred to as its natural comparison method.

Lists (and arrays) of objects that implement this interface can be sorted automatically by Collections.sort (and Arrays.sort). Objects that implement this interface can be used as keys in a sorted map or as elements in a sorted set, without the need to specify a comparator.

The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C. Note that null is not an instance of any class, and e.compareTo(null) should throw a NullPointerException even though e.equals(null) returns false.

It is strongly recommended (though not required) that natural orderings be consistent with equals. This is so because sorted sets (and sorted maps) without explicit comparators behave "strangely" when they are used with elements (or keys) whose natural ordering is inconsistent with equals. In particular, such a sorted set (or sorted map) violates the general contract for set (or map), which is defined in terms of the equals method.

For example, if one adds two keys a and b such that (!a.equals(b) && a.compareTo(b) == 0) to a sorted set that does not use an explicit comparator, the second add operation returns false (and the size of the sorted set does not increase) because a and b are equivalent from the sorted set's perspective.

Virtually all Java core classes that implement Comparable have natural orderings that are consistent with equals. One exception is java.math.BigDecimal, whose natural ordering equates BigDecimal objects with equal values and different precisions (such as 4.0 and 4.00).

For the mathematically inclined, the relation that defines the natural ordering on a given class C is:

       {(x, y) such that x.compareTo(y) <= 0}.
 

The quotient for this total order is:

       {(x, y) such that x.compareTo(y) == 0}.
 

It follows immediately from the contract for compareTo that the quotient is an equivalence relation on C, and that the natural ordering is a total order on C. When we say that a class's natural ordering is consistent with equals, we mean that the quotient for the natural ordering is the equivalence relation defined by the class's equals(Object) method:

     {(x, y) such that x.equals(y)}. 

This interface is a member of the Java Collections Framework.
        由文档可知,一旦类实现了Comparable接口,那么就表明它的实例具有内在的排序关系,那么对于这样的类的对象排序就很简单的可以使用一些对象的sort方法,如

Arrays.sort(a);
        其实为了提供比较顺序而实现的Comparable接口,它规定的compareTo方法会在两个对象进行比较的时候被调用,以返回结果来确定对象间的顺序关系,对于存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也同样简单,例如下面的程序信赖于String实现了Comparable接口,它去掉了命令行参数列表中的重复参数,并按字母顺序打印出来:

public class WordList {
	public static void main(String[] args) {
		Set<String> s = new TreeSet<String>();
		Collections.addAll(s, args);
		System.out.println(s);
	}
}
        一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作,Java平台类库中的所有值类都实现了Comparable接口。如果编写一个値类,它具有非常明显的内在排序关系,比如按字母排序、按数值排序或者按年代排序,那么就应该坚决考虑实现这个接口。

        CompareTo的约定如下:

        将这个对象与指定的对象进行比较。当该对象小于、等于或者大于指定对象的时候,分别返回一个负整数,零或者正整数,如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException。

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

        1、实现者必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。这也表明当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才必须抛出异常。

        2、实现者必需要确保这个比较关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)则有x.compareTo(z) > 0。

        3、实现者必须确保x.compareTo(y) == 0暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。

        4、强烈建议(x.compareTo(y) == 0 ) == (x.equals(y)),但是这并非绝对必要的。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应该予以说明,推荐使用这样的说法:“Note: this class has a natural ordering that is inconsistent with equals.”

        在类的内部,任何合理的顺序关系都可以满足compareTo约束,与equals不同的是,在跨越不同的类的时候,compareTo可以不做比较:如果两个被比较的对象引用不同类的对象,comapreTo可以抛出ClassCastException。通常这正是compareTo在这种情况下应该做的事情,如果类设置了正确的参数,这也正是它所要做的事情。

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

        注意:无法在用新的值组件扩展可实例化的类时,同时保持compareTo约定,这样有些类似于c++里的重载操作符,更好利用面向对象编程。如果想为一个实现了Comparable接口的类增加值组件,请不要扩展这个类,因为这也许会碰到和equals中一样的问题,破坏了约束关系传递性。可以编写民一个不相关的类,其中包含每一个类的实例,然后提供一个规则方法返回这个实例,这样既可以自由地在第二个类上实现compareTo方法,同时也允许它的客户端在必要的时候,把第二个类的实例视同第一个类的实例。

        约束的最后一条是个建议,并非强制。但是如果不尽量遵守这条约束的话会出现让人感觉很奇怪的事情,如果一个类的compareTo方法施加了一个与equals方法不一致的顺序关系,它仍可能正常工作,但是,如果一个有序集合包含了该类的元素,这个集合就可能无法遵守相应集合的接口的通用约定,为什么会出现这样的问题?因为对于这些接口的通用约定是按照equals方法来定义的,但是有序集合使用由compareTo方法而不是equals方法来所施加的等同测试,这样一来接口的规范和实现的规则不相同,就可能出现明明应该是equals的对象,在用compareTo来判断的时候出现不相等,导致集合的数据与所期望的不同。

        上面的文档中指出了java.math.BigDecimal类中它的compareTo方法和equals方法不一致,如果创建了一个HashSet实例,并添加new BigDecimal("4.0")和new BigDecimal("4.00"),那么这个集合就包括这两个对象,很奇怪不是吗?那,首先来看看文档中关于HashSet的add方法的说明:

public boolean add(E e)

Adds the specified element to this set if it is not already present. More formally, adds the specified element e to this set if this set contains no element e2 such that (e==null ? e2==null : e.equals(e2)). If this set already contains the element, the call leaves the set unchanged and returns false.
        可以看出,HashSet在添加元素的时候是调用对象的equals方法来判断元素是否则在的,再来看看BigDecimal的equals方法:

@Override
    public boolean equals(Object x) {
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
        if (scale != xDec.scale)
            return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) {
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
        } else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);

        return this.inflate().equals(xDec.inflate());
    }
        下面这段代码是BigDecimal的compareTo方法:

    public int compareTo(BigDecimal val) {
        // Quick path for equal scale and non-inflated case.
        if (scale == val.scale) {
            long xs = intCompact;
            long ys = val.intCompact;
            if (xs != INFLATED && ys != INFLATED)
                return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
        }
        int xsign = this.signum();
        int ysign = val.signum();
        if (xsign != ysign)
            return (xsign > ysign) ? 1 : -1;
        if (xsign == 0)
            return 0;
        int cmp = compareMagnitude(val);
        return (xsign > 0) ? cmp : -cmp;
    }
        再看看构造函数中关于scale的初始化

if (dot)
        ++scl;
        continue;

最后:
this.scale = scl;
        dot是个flag,当扫描字符串的时候第一次遇到‘.’的时候被置真,这样,虽然两个BigDecimal对象的值在直观上看是相等的,但是由于equals函数的实现(HashSet调用equals进行比较),当两个对象的scale不同的时候返回了假,但是对于compareTo方法安全的忽略了这个问题,因此测试这两个对象,compareTo方法返回的是0。然而如果使用TreeSet而不是HashSet来执行同样的任务,那么集合中就只有一个对象,原因在于TreeSet的比较方法采用的是对象的compareTo方法,通过查看源码,当new一个TreeSet的时候如果不提供参数,那么就会默认使用一个实现了NavigableMap<K,V>接口的TreeMap来当做容器,而这个TreeMap也是一个实现了NavigableMap<K,V>接口的类,重写了put方法,使用的比较方法是compareTo,再综合上面讨论的BigDecimal类中equals与compareTo的区别的讨论就可以解释为什么会出现这种奇怪的现象,这就是compareTo与equals不一致会导致的问题的一个很好的例子。

        编写compareTo方法与编写equals方法非常相似,但也存在几个重大的差别,因为Compareable接口是参数化的,而且compareable方法是静态的类型,因此不必进行类型检查,也不必对它的参数进行类型转换。如果参数的类型不合适,这个调用甚至是无法编译的。如果参数为空,那么这个调用应该抛出NullPointException,虽然当参数为空时equals返回false。

        CompareTo方法中的域的比较是顺序比较,而不是等同性的比较,比较对象引用域可以是通过递归地调用compareTo方法来实现,如果一个域没有实现Compareable接口的话,或者需要一个非标准的方法来实现,那么就可以使用一个显式的Comparator来代替,或者编写新的Comparator:

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
	public int compareTo(CaseInsensitiveString cis) {
		return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
	}
}
        上面这段在compareTo方法中调用了String类的compare方法,来完成忽略大小写的比较,这里国为CaseInsensitiveString实现了Compareable接口,所以他只能与其他的Compareable<CaseInsensitiveString>进行比较,声名类去实现Comparable接口时,这是常用的模式。还要注意的是compareTo方法的参数是CaseInsensitiveString,而不是Object ,这也是和equals的一个重要区别。

        比较整数型基本类型的域,可以使用关系操作符< 和 >,但是浮点域要用Double.compare或者Float.compare这些Comparator来进行比较,因为他们没有遵守compareTo的约定,对于数组域来说,则要把这些指导原则应用到每个元素上。如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。必须从最关键的域开始逐渐比较到所有的重要域。如果某个域产生了非零的结果,则整个比较就结束了,如果最关键的域相等,则进一步比较次重要的关键域,如果所有的域都是相等的,那么对象才是相等的。这是出于一种效率和重要度的考虑,考虑下面的例子:

public int compareTo(PhoneNumber pn) {
	// Compare area codes
	if (areaCode < pn.areaCode) 
		return -1;
	if (areaCode > pn.areaCode)
		return 1;
	// Area codes are equal ,compare prefixs
	if (prefix < pn.prefix)
		return -1;
	if (prefix > pn.prefix)
		return 1;
	// Area codes and prefixs are equal, compare line numbers
	if (lineNumber < pn.lineNumber)
		return -1;
	if (lineNumber > pn.lineNumber)
		return 1;
	return 0;
}
        如果是基本类型,也可以利用域之间的差值来判断大小关系,但是这种方法有一种限制,就是要确定相关的域中不会出现负数,或者最小和最大的可能域值之差小于或等于INTEGER.MAX_VALUE,否则就不能用这种方法。因为一个有符号的32位整数还没有大到足以表达任意两个32位整数的差。如果i是一个很大的正数,而j是一个很大的负数,那么i-j就会溢出(假设i和j都是int类型),并且返回一个负值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值