第8条:覆盖equals时请遵守通用约定
覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。
那么,什么时候应该覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖
equals方法。这通常属于“值类(value class)"的情形。值类仅仅是一个表示值的类,例如Integer或者Date。程序员在利用equals方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是想了解它们是否指向同一个对象。
equals方法实现了等价关系(equivalence relation ) :
· 自反性(reflexive)。对于任何非null引用值x, x.equals(x)须返回true。
· 对称性(symmetric)。对于任何非null的引用值x和y,当且仅当y.equals(x]返回true时,x.equals(y)必须返回true。
· 传递性(transitive)。对于任何非null的引用值x, y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。
· 一致性(consistent)。对干任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true, 或者一致地返回false。
· 对于任何非null的引用值x, x.equals(null)必须返回false。
我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,除非愿意放弃面向对象的抽象所带来的优势。
虽然没有一种令人满意的办法可以既扩展不可实例化的类,又增加值组件,但还是有一种不错的权宜之计(workaround)。根据第16条的建议:复合优先于继承。
得出以下实现高质量equals方法的诀窍
- 使用==操作符检查“参数是否为这个对象的引用”。
- 使用instanceof操作符检查“参数是否为正确的类型”。
- 把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
- 对于该类中的每个‘’关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。
对干既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。对Float和double域进行特殊的处理是有必要的,因为存在着Float..NaN、-0.0f以及类似的double常量;详细信息请参考Float.equals的文档。
- 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的。
关于equals方法的警告:
- 覆盖equals时总要覆盖hashCode。
- 不要企图让equals方法过于智能。
- 不要将equals声明中的Object对象(参数)替换为其他的类型。
第9条:覆盖equals时总要覆盖hashCode
下面是约定的内容,摘自Object规范[Java SE 6]
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
- 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
- 如果两个对象根据equals(Object)方法比较是不相等的。那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。
Note:
HashMap有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不匹配,也不必检验对象的等同性。
在散列码的计算过程中,可以把冗余域(redundant field)排除在外。换句话说,如果一个域的值可以根据参与计算的其他域值计算出来,则可以把这样的域排除在外。必须排除equals比较计算中没有用到的任何域。否则很有可能违反hashCode约定的第二条。
如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果你觉得这种类型的大多数对象会被用做散列键(hash keys ), 就应该在创建实例的时候计算散列码。否则,可以选择“延迟初始化(lazily initialize )”散列码,一直到hashCode被第一次调用的时候才初始化(见第71条)。
第10条:始终要覆盖toString
如果指定了字符串的格式,好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地在对象和它的字符串表示法之间来回转换。Java平台类库中的许多值类都采用了这种做法,包括BigInteger、BigDecimal和绝大多数的基本类型包装类( boxed primitive class )。
无论你是否决定指定格式,都应该在文档中明确地表明你的意图。如果你要指定格式,则应该严格地这样去做。
第11条: 谨慎地覆盖clone
Cloneable接口的目的是作为对象的一个mixin接n口 (mixin interface)(见第18条),表明这样的对象允许克隆( clone)。遗憾的是,它并没有成功地达到这个目的。其主要的缺陷在于,它缺少一个clone方法,Object的clone方法是受保护的。如果不借助于反封(reflection)(见第53条),就不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法。即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的clone方法。
既然Cloneable并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为: 如果一个类实现了Cloneable, Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedExcepion异常。这是接口的一种极端非典型的用法,也不值得仿效。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneable接口,它改变了超类中受保护的方法的行为。
如果你扩展一个实现了Cloneable接口的类,那么你除了实现一个行为良好的clone方法外,没有别的选择。否则, 最好提供某些其他的途径来代替对象拷贝,或者干脆不提供这样的功能。例如,对于不可变类,支持对象拷贝并没有太大的意义,因为被拷贝的对象与原始对象并没有实质的不同。
另一个实现时象拷贝的好办法是提供一个拷贝构造器(copy constructor)或拷贝工厂(copy factory)。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,例如:
有些专家级的程序员干脆从来不去覆盖clone方法,也从来不去调用它,除非拷贝数组。你必须清楚一点,对于一个专门为了继承而设计的类,如果你未能提供行为良好的受保护的(protected )clone方法,它的子类就不可能实现Cloneable接口。
第12条: 考虑实现Comparable接口
如果你正在编写一个值类,它具有非常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现这个接口:
将这个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。