第3章对于所有对象都通用的方法¶
第8条:覆盖equals时请遵守通用约定¶
在覆盖equals方法的时候,你必须要遵守它的通用约定。
- 自反性(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约定,当其他对象面对你的对象时,你完全不知道这些对象的行为会怎么样。
为了解决这个问题,只需把企图与String互操作的这段代码从equals方法中去掉就可以了。
扩展这个类,为一个点添加颜色信息:
如果完全不提供equals方法,而是直接从Point继承过来,在equals做比较的时候颜色信息就被忽略掉了。编写一个equals方法,只有当它的参数是另一个有色点,并且具有同样的位置和颜色时,它才会返回true:
这个方法的问题在于,你在比较普通点和有色点,以及相反的情形时,可能会得到不同的结果。前一种比较忽略了颜色信息,而后一种比较则总是返回false,因为参数的类型不正确。
可以做这样的尝试来修正这个问题,让ColorPoint.equals在进行“混合比较”时忽略颜色信息。
这种方法确实提供了对称性,但是却牺牲了传递性:
怎么解决呢?事实上,这是面向对象语言中关于等价关系的一个基本问题。我们**无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,**除非愿意放弃面向对象的抽象所带来的优势。
在equals方法中用getClass测试替代instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定:
这段程序只有当对象具有相同的实现时,才能使对象等同。
虽然没有一种令人满意的方法可以既扩展不可实例化的类,又增加值组件,但还是有一种不错的权宜之计。根据第16条建议:复合优先于继承。我们不再让ColorPoint扩展Point,而是在ColorPoint中加入一个私有的Point域,以及一个公有的视图方法,此方法返回一个与该有色点处于相同位置的普通Point对象。
结合所有这些要求,得出了以下实现高质量equals方法的诀窍:
- 使用==操作符检查“参数是否为这个对象的引用”。
- 使用
instanceof操作符检查“参数是否为正确的类型”。 - 把参数转换成正确的类型。
- 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
- 当编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?
下面是最后的告诫:
- 覆盖equals时总要覆盖hashCode。
- 不要企图让
equals方法过于智能。 - 不要让equals声明中的Object对象替换为其他的类型。
第9条:覆盖equals时总要覆盖hashCode¶
一个很常见的错误根源在于没有覆盖hashCode方法。在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违法hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。
下面是约定的内容,摘自Object规范。
- 在应用程序的执行期间,只要对象的
equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。 - 如果两个对象根据
equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。 - 如果两个对象根据
equals方法比较是不相等的,那么调用这两个对象任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”。这正是hashCode约定中第三条的含义。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。要想完全达到这种理想的情形是非常困难的。幸运的是,相对接近这种理想情形则并不太困难。下面给出一种简单的解决方法:
- 把某个非零的常数值,比如说17,保存在一个名为
result的int类型的变量中。 - 对于对象中每个关键域f,完成以下步骤: 1. 为该域计算int类型的散列码 1. 如果该域是boolean类型,则计算(f?1:0)。 2. 如果该域是byte、char、short或者int类型,则计算\(int\)f。 3. 如果该域是long类型,则计算\(int\)\(f^\(f>>>32\))。 4. 如果该域是float类型,则计算
Float.floatToIntBits(f)。 5. 如果该域是double类型,则计算Double.doubleToLongBits(f),然后为得到的long类型计算散列值。 6. 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方法来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0。 7. 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.2中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发型版本1.5中增加的其中一个Arrays.hashCode方法。 2. 按照下面的公式,把步骤2.1中计算得到的散列码c合并到result中:
result = 31 * result + c;
- 返回result
- 写完hashCode方法之后,问问自己“相等的实例是否具有相等的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。
第10条:始终要覆盖toString¶
虽然java.lang.Object提供了toString方法的一个实现。但它返回的字符串通常并不是用户所期望看到的。它包含类的名称,以及一个“@”符号,接着是散列码的无符号十六进制表示法。toString的通用约定指出,被返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”。toString的约定进一步指出,“建议所有的子类都覆盖这个方法”。
提供好的toString实现可以使类用起来更加舒适。当对象传递给println、printf、字符串联操作符(+)以及assert或者被调试器打印出来时,toString方法会被自动调用。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。
第11条:谨慎地覆盖clone¶
Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆(clone)。遗憾的是,它并没有成功地达到这个目的。其主要的缺陷在于,它缺少一个clone方法,Object的clone方法是受保护的(protected)。如果不借助反射,就不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法。
既然Cloneable并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。
如果实现Cloneable接口是要对某个类起到作用,类和它的所有超类都必须遵守一个相当复杂的,不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的机制:无需调用构造器就可以创建对象。
第12条:考虑实现Comparable接口¶
compareTo方法并没有在Object中声明。相反,它是Comparable接口中唯一的方法。compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特征,它还是个泛型。类实现了Comparable接口的对象数组进行排序就这么简单: