Effective Java 第三版——14.考虑实现Comparable接口
Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。
在这里第一时间翻译成中文版。供大家学习分享之用。
14.考虑实现Comparable接口
与本章讨论的其他方法不同,compareTo
方法并没有在Object
类中声明。 相反,它是Comparable
接口中的唯一方法。 它与Object
类的equals
方法在性质上是相似的,除了它允许在简单的相等比较之外的顺序比较,它是泛型的。 通过实现Comparable
接口,一个类表明它的实例有一个自然顺序( natural ordering)。 对实现Comparable
接口的对象数组排序非常简单,如下所示:
Arrays.sort(a);
它很容易查找,计算极端数值,以及维护Comparable
对象集合的自动排序。例如,在下面的代码中,依赖于String
类实现了Comparable
接口,去除命令行参数输入重复的字符串,并按照字母顺序排序:
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
通过实现Comparable
接口,可以让你的类与所有依赖此接口的通用算法和集合实现进行互操作。 只需少量的努力就可以获得巨大的能量。 几乎Java平台类库中的所有值类以及所有枚举类型(条目 34)都实现了Comparable
接口。 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现Comparable
接口:
public interface Comparable<T> {
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
。最后,对于所有的z,实现类必须确保
[x.compareTo(y) == 0
意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z))
。强烈推荐
x.compareTo(y) == 0) == (x.equals(y))
,但不是必需的。 一般来说,任何实现了Comparable
接口的类违反了这个条件都应该清楚地说明这个事实。 推荐的语言是“注意:这个类有一个自然顺序,与equals
不一致”。
与equals
方法一样,不要被上述约定的数学特性所退缩。这个约定并不像看起来那么复杂。 与equals
方法不同,equals
方法在所有对象上施加了全局等价关系,compareTo
不必跨越不同类型的对象:当遇到不同类型的对象时,compareTo
被允许抛出ClassCastException
异常。 通常,这正是它所做的。 约定确实允许进行不同类型间比较,这种比较通常在由被比较的对象实现的接口中定义。
正如一个违反hashCode
约定的类可能会破坏依赖于哈希的其他类一样,违反compareTo
约定的类可能会破坏依赖于比较的其他类。 依赖于比较的类,包括排序后的集合TreeSet
和TreeMap
类,以及包含搜索和排序算法的实用程序类Collections
和Arrays
。
我们来看看compareTo
约定的规定。 第一条规定,如果反转两个对象引用之间的比较方向,则会发生预期的事情:如果第一个对象小于第二个对象,那么第二个对象必须大于第一个; 如果第一个对象等于第二个,那么第二个对象必须等于第一个; 如果第一个对象大于第二个,那么第二个必须小于第一个。 第二项约定说,如果一个对象大于第二个对象,而第二个对象大于第三个对象,则第一个对象必须大于第三个对象。 最后一条规定,所有比较相等的对象与任何其他对象相比,都必须得到相同的结果。
这三条规定的一个结果是,compareTo
方法所实施的平等测试必须遵守equals
方法约定所施加的相同限制:自反性,对称性和传递性。 因此,同样需要注意的是:除非你愿意放弃面向对象抽象(条目 10)的好处,否则无法在保留compareTo
约定的情况下使用新的值组件继承可实例化的类。 同样的解决方法也适用。 如果要将值组件添加到实现Comparable
的类中,请不要继承它;编写一个包含第一个类实例的不相关的类。 然后提供一个返回包含实例的“视图”方法。 这使你可以在包含类上实现任何compareTo
方法,同时客户端在需要时,把包含类的实例视同以一个类的实例。
compareTo
约定的最后一段是一个强烈的建议,而不是一个真正的要求,只是声明compareTo
方法施加的相等性测试,通常应该返回与equals
方法相同的结果。 如果遵守这个约定,则compareTo
方法施加的顺序被认为与equals
相一致。 如果违反,顺序关系被认为与equals
不一致。 其compareTo
方法施加与equals
不一致顺序关系的类仍然有效,但包含该类元素的有序集合可能不服从相应集合接口(Collection
,Set
或Map
)的一般约定。 这是因为这些接口的通用约定是用equals
方法定义的,但是排序后的集合使用compareTo
强加的相等性测试来代替equals
。 如果发生这种情况,虽然不是一场灾难,但仍是一件值得注意的事情。
例如,考虑BigDecimal
类,其compareTo
方法与equals
不一致。 如果你创建一个空的HashSet
实例,然后添加new BigDecimal("1.0")
和new BigDecimal("1.00")
,则该集合将包含两个元素,因为与equals
方法进行比较时,添加到集合的两个BigDecimal
实例是不相等的。 但是,如果使用TreeSet
而不是HashSet
执行相同的过程,则该集合将只包含一个元素,因为使用compareTo
方法进行比较时,两个BigDecimal
实例是相等的。 (有关详细信息,请参阅BigDecimal
文档。)
编写compareTo
方法与编写equals
方法类似,但是有一些关键的区别。 因为Comparable
接口是参数化的,compareTo
方法是静态类型的,所以你不需要输入检查或者转换它的参数。 如果参数是错误的类型,那么调用将不会编译。 如果参数为null,则调用应该抛出一个NullPointerException
异常,并且一旦该方法尝试访问其成员,它就会立即抛出这个异常。
在compareTo
方法中,比较属性的顺序而不是相等。 要比较对象引用属性,请递归调用compareTo
方法。 如果一个属性没有实现Comparable
,或者你需要一个非标准的顺序,那么使用Comparator
接口。 可以编写自己的比较器或使用现有的比较器,如在条目 10中的CaseInsensitiveString
类的compareTo
方法中:
// Single-field Comparable with object reference field
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_[ORDER.compare(s](http://ORDER.compare(s), cis.s);
}
... // Remainder omitted
}
请注意,CaseInsensitiveString
类实现了Comparable <CaseInsensitiveString>
接口。 这意味着CaseInsensitiveString
引用只能与另一个CaseInsensitiveString
引用进行比较。 当声明一个类来实现Comparable
接口时,这是正常模式。
在本书第二版中,曾经推荐如果比较整型基本类型的属性,使用关系运算符“<” 和 “>”,对于浮点类型基本类型的属性,使用Double.compare
和[Float.compare
静态方法。在Java 7中,静态比较方法被添加到Java的所有包装类中。 在compareTo
方法中使用关系运算符“<” 和“>”是冗长且容易出错的,不再推荐。
如果一个类有多个重要的属性,那么比较他们的顺序是至关重要的。 从最重要的属性开始,逐步比较所有的重要属性。 如果比较结果不是零(零表示相等),则表示比较完成; 只是返回结果。 如果最重要的字段是相等的,比较下一个重要的属性,依此类推,直到找到不相等的属性或比较剩余不那么重要的属性。 以下是条目 11中PhoneNumber
类的compareTo
方法,演示了这种方法:
// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
if (result == 0) {
result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
if (result == 0)
result = [Short.compare(lineNum](http://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<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return [COMPARATOR.compare(this](http://COMPARATOR.compare(this), pn);
}
此实现在类初始化时构建比较器,使用两个比较器构建方法。第一个是comparingInt
方法。它是一个静态方法,它使用一个键提取器函数式接口( key extractor function)作为参数,将对象引用映射为int类型的键,并返回一个根据该键排序的实例的比较器。在前面的示例中,comparingInt
方法使用lambda表达式,它从PhoneNumber
中提取区域代码,并返回一个Comparator<PhoneNumber>
,根据它们的区域代码来排序电话号码。注意,lambda表达式显式指定了其输入参数的类型(PhoneNumber pn)
。事实证明,在这种情况下,Java的类型推断功能不够强大,无法自行判断类型,因此我们不得不帮助它以使程序编译。
如果两个电话号码实例具有相同的区号,则需要进一步细化比较,这正是第二个比较器构建方法,即thenComparingInt
方法做的。 它是Comparator
上的一个实例方法,接受一个int类型键提取器函数式接口( key extractor function)作为参数,并返回一个比较器,该比较器首先应用原始比较器,然后使用提取的键来打破连接。 你可以按照喜欢的方式多次调用thenComparingIn
t方法,从而产生一个字典顺序。 在上面的例子中,我们将两个调用叠加到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<Object> 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<Object> 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<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
总而言之,无论何时实现具有合理排序的值类,你都应该让该类实现Comparable
接口,以便在基于比较的集合中轻松对其实例进行排序,搜索和使用。 比较compareTo
方法的实现中的字段值时,请避免使用"<"和">"运算符。 相反,使用包装类中的静态compare
方法或Comparator
接口中的构建方法。
Effective Java 第三版——14.考虑实现Comparable接口的更多相关文章
- Effective Java 第三版——21. 为后代设计接口
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——64. 通过对象的接口引用对象
Tips 书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code 注意,书中的有些代码里方法是基于Java 9 API中的,所 ...
- 《Effective Java 第三版》新条目介绍
版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...
- 《Effective Java 第三版》目录汇总
经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...
- effective Java 第三版学习笔记
创建对象类型的 1,静态工厂方法代替构造器 静态工厂方法有名称,不容易混乱他的作用 不必再每次调用他的时候创建实例,创建实例的代价是高的,可以重复利用缓存的对象 静态工厂甚至能返回子类对象,例如在接口 ...
- Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——30. 优先使用泛型方法
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——34. 使用枚举类型替代整型常量
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
- Effective Java 第三版——46. 优先考虑流中无副作用的函数
Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...
随机推荐
- Linux常用命令及部分详解
1.总结部分 常用指令 ls 显示文件或目录 -l 列出文件详细信息l(list) -a 列出当前目录下所有文件及目录,包括隐藏的a(all) m ...
- 关于ftp的学习:ftp很多人都会用。但会用,不代表我们真正了解它。
ftp.sftp.ftps,您是否是也跟我一样搞不清楚他们的真正意义.它们之间有关联吗(究竟是何种关联),有区别吗(区别倒地在哪里). 亦或是以为自己知道它们,可我们真的了解并认识它们了吗? 如果您被 ...
- String、StringBuilder和StringBuffer
1.string不可变性 java的docs有这样一句话:Strings are constant; their values cannot be changed after they are cre ...
- HDU1075-What Are You Talking About
What Are You Talking About Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 102400/204800 K ...
- centos7 忘记mysql root密码办法
1.首先确认服务器出于安全的状态,也就是没有人能够任意地连接MySQL数据库. 因为在重新设置MySQL的root密码的期间,MySQL数据库完全出于没有密码保护的状态下,其他的用户也可以任意地登录和 ...
- 处理eclipse启动时报java.lang.IllegalStateException
这是我写的第一篇博客,博客我来了: 我是好学的人,希望在这上面遇到志同道合的人,对技术有更高追求的人: 重启eclipse的时候报出来 An error has occurred, See the l ...
- 过渡与动画 - steps调速函数&CSS值与单位之ch
写在前面 上一篇中我们熟悉五种内置的缓动曲线和(三次)贝塞尔曲线,并且基于此完成了缓动效果. 但是如果我们想要实现逐帧动画,基于贝塞尔曲线的调速函数就显得有些无能为力了,因为我们并不需要帧与帧之间的过 ...
- WebLogic部署报java.lang.ClassCastException: weblogic.xml.jaxp.RegistrySAXParserFactory cannot be cast to javax.xml.parsers.SAXParserFactory
今天在部署WebLogic项目时,报了java.lang.ClassCastException: weblogic.xml.jaxp.RegistrySAXParserFactory cannot b ...
- 》》mui--图片轮播
mui框架内置了图片轮播插件,通过该插件封装的JS API,用户可以设定是否自动轮播及轮播周期,如下为代码示例: //获得slider插件对象 var gallery = mui('.mui-slid ...
- Winform开发框架中工作流模块的表设计分析
在较早博客随笔里面写过文章<Winform开发框架之简易工作流设计>之后,很久没有对工作流部分进行详细的介绍了,本篇继续这个主题,详细介绍其中的设计.实现及效果给大家,这个工作流在好几年前 ...