您的位置:首页 > Web前端

【Effective Java中文版】第二版:第三章 对于所有对象都通用的方法[003] [20180110]

2018-01-10 17:43 519 查看
第12条:考虑实现Comparable接口

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

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

Set<String> s = new TreeSet<String>();
Collections.addAll(s, args);
System.out.println(s);
一旦类实现了这个接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。你付出很小的努力就有获得非常强大的功能。事实上,Java平台类库中的所有值类
(value class)都实现了这个接口,如果你正在编写一个类,它具有非常明显的内在排序关系,那你就应该坚决考虑实现这个接口:

public interface Comparable<T> {
public int compareTo(T o);
}
compareTo方法的通用约定与equals相似:

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

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

·实现者必须满足所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(n))。这也暗示着,当且仅当x.compareTo(y)抛出异常时,y.compareTo(x))才必须抛出异常

·实现者还必须确保这个比较关系是可传递的:x.compareTo(y) > 0 && y.compareTo(z)) >0,暗示着x.compareTo(z) > 0

·最后实现者必须确保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约定。与equals不同的是,在跨越不同类的时候,compareTo可以不做比较,如果两个被比较的对象引用了不同类的对象,compareTo可以抛出ClassCastException。通常这正是compareTo在这种情况下应该做的事情,如果类设置了正确的参数,这也是它要做的事情。虽然以上约定并没有把跨类之间的比较排除在外,但是从Java1.6发行版开始,Java平台类库中就没有哪个类有支持这个特性了。

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

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

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

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

例如,考虑BigDecimal类,它的compareTo方法与equals方法不一致。如果你创建一个HaseSet实例,并且添加new BigDecimal("1.0")和new BigDecimal("1.00"),这个集合就将包含两个元素,因为新增到集合中的两个BigDecimal实例,通过equals方法比较是不想等的。然而,如果你使用TreeSet来执行同样的过程,集合中将只包含一个元素,因为这两个BigDecimal实例通过compareTo方法进行比较时是相等的。

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

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

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString>{

private final String s;

public CaseInsensitiveString(String s) {
super();
if(null == s)
throw new NullPointerException();
this.s = s;
}

@Override
public int compareTo(CaseInsensitiveString cis) {

return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}

}

注意C阿瑟InsensitiveString类实现了Comparable<CaseInsensitiveString>接口。由此可见, CaseInsensitiveString引用只能与其他的Comparable<CaseInsensitiveString>引用进行比较。在声明类去实现Comparable接口时,这是常用的模式。还要注意compareTo方法的参数是CaseInsensitiveString而不是object。这是上述的类声明所要求的。
比较整数型基本类型的域,可以使用关系符 < 和 >。例如浮点用Double.compare或者Float.compare。而不用关系操作符,当应用到浮点值时,他们没有遵守compareTo的约定。对于数组域,则要把这些指导原则应用到每个元素上。

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

@Override
public int compareTo(PhoneNumber pn) {
// Compare area codes
if(areaCode < pn.areaCode)
return -1;
if(areaCode > pn.areaCode)
return 1;
// Area code are equal, compare prefixes
if(prefix < pn.prefix)
return -1;
if(prefix > pn.prefix)
return 1;
// Area code and prefix are equal, compare line numbers
if(lineNumber < pn.lineNumber)
return -1;
if(lineNumber > pn.lineNumber)
return 1;

return 0;// All fields are equal
} 虽然这个方法可行,但它还可以进行改进,回想一下,compareTo方法的约定并没有指定返回值的大小,而只是指定了返回值的符号,你可以利用这一点来简化代码,或者还可以提高它的运行速度
@Override
public int compareTo(PhoneNumber pn) {
// Compare area codes
int areaCodeDiff = areaCode - pn.areaCode;
if(areaCodeDiff != 0)
return areaCodeDiff;

// Area code are equal, compare prefixes
int prefixDiff = prefix - pn.prefix;
if(prefixDiff != 0)
return prefixDiff;

// Area code and prefix are equal, compare line numbers
return lineNumber - pn.lineNumber;
}


这项技巧在这里能够工作得很好,但是用起来要非常小心。除非你确信相关的域不会为负值,或者更一般的情况:最小和最大的可能域值之差小于或等于Integer.MAX_VALUE, 否则就不要使用这种方法。这项技巧有时不能正常工作得原因在于,一个有符号的32位整数还么有大到足以表达任何两个32位整数的差。如果i是一个很大的整数,而j是一个很大的负整数, 那么i-j就会溢出,并返回一个负值。这样就是的compareTo方法将对某些参数返回错误的结果,违反了compareTo约定的第一条和第二条。这不是一个纯粹的理论问题:它已经在实际的系统中导致了失败。这些失败可能非常难以调试,因为这样的compareTo方法对于大多数的输入值都能够正常工作、
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息