Effective Java:对于所有的对象都通用的方法

时间:2022-12-03 16:01:44

Java中所有的类都默认继承Object类,而Object的设计主要是为了扩展。它的所有的非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定(general contract),因为它们被设计成要被覆盖的(override)。任何一个类,它在覆盖这些方法的时候都有责任遵守这些通用的约定;如果不能做到这一点,其他依赖于这些约定的类就无法结合该类一起正常运作。

本章将讲述何时以及如何覆盖这些非final的Object方法。本章不再讨论finalize方法,因为前一篇文章已经讨论过了。而Comparable.compareTo虽然不是Object方法,但是本章也对它进行讨论,因为它具有类似的特征。

第八条:覆盖equals时请遵守通用约定

在覆盖equals方法的时候,必须遵守它的通用约定。下面是约定内容,来自Object的规范[Java SE6]:

equals方法实现了等价关系(equivalence relation):

  • 自反性。对于任何非null的引用值x,x.euqals(x)必须返回true。
  • 对称性。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。
  • 一致性。对于任何非null的引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,多次调用x.equals(y)的结果应该保持一致。
  • 对于任何非null的引用值x,x.equals(null)必须返回false。

没有哪个类是孤立的。一个类的实例通常会被频繁地传递给另一个类的实例。有许多类,包括所有的集合类在内,都依赖于传递给他们的对象是否遵守了equals约定。如果违反了它们,程序就会表现的不正常,甚至崩溃,而且很难找到失败的根源。

结合上述要求,得出了以下实现高质量equals方法的诀窍:

  • 使用==操作符检查“参数是否为这个对象的引用”。
  • 使用instanceof操作符检查“参数是否为正确的类型”。
  • 把参数转换成正确的类型。
  • 对于该类中的每个“关键(significant)域,检查参数中的域是否与该对象中对应的域相匹配。
  • 当你编写完成了equals方法之后,就应该问自己:它是否是对称的、传递的、一致的?

下面是最后的一些告诫:
- 覆盖equals时总要覆盖hashCode(见第九条)。
- 不要企图让equals方法过于智能。
- 不要讲equals声明中的Object对象替换为其他的类型。

第九条:覆盖equals时总要覆盖hashCode

每个覆盖了equals方法的类中,也必须覆盖hashCode方法,否则就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,如HashMap、HashSet和HashTable等。

下面是约定的内容,摘自Object规范[Java SE6]:

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行中,每次执行所返回的整数可以不一致。
  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能(详情请参考HashMap散列原理)。

因为没有覆盖hashCode而违反的关键约定是第二条:相等的对象必须具有相等的散列值(hash code)。根据累的 equals方法,两个截然不同的实例在逻辑上有可能是相等的,但是,根据Object类的hashCode方法,它们仅仅是两个没有任何共同之处的对象。因此,对象的hashCode方法返回两个看起来是随机的整数,而不是根据第二个约定所要求的那样,返回两个相等的整数。

下面给出一种简单的重写hashCode的方法,这个方法符合在符合第二约定的同时也满足了第三约定:

  1. 把某个非零的常数值,比如17,保存在一个名为result的int类型的变量中。
  2. 对于对象中的每个关键域f(指equals方法中涉及到的每个域),完成以下步骤:
    a. 为该域计算int类型的散列码c:
    i. 如果该域是boolean类型,则计算(f?1:0).
    ii.如果该域是byte、char、short或者int类型,则计算(int)f。
    iii.如果该域是long类型,则计算(int)(f^(f>>>32))。
    iv.如果该域是float类型,则计算Float.floatToIntBits(f)。
    v.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤iii,为得到的long类型值计算散列值。
    vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个”范式(canonical representation)“,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
    viii. 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的方法把这些散列值组合起来。如果数组域中国的每个元素都很重要,可以利用JDK中的一个Arrays.hashCode方法。
    b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:
    result = 31 * result + c;
  3. 返回result。
  4. 写完了hashCode方法之后,自问一些”相等的实例是否都具有相等的散列码“。要是不相等,需要找到原因,并修正错误。

书中说步骤1中用到了一个非零的初始值,因此步骤2.a中计算的散列值为0的那些初始域,会影响到散列值,如果步骤1中的初始值为0, 则整个散列值将不受这些初始域的影响。

第十条:始终要覆盖toString

如果不覆盖toString方法,调用x.toString的时候打印出来的将是”类名@散列码“,例如PhoneNumber@163b91。toString的通用约定之处,被返回的字符串应该是一个”简洁的,但信息丰富,并且易于阅读的表达形式[Java SE6],建议所有的子类都覆盖这个方法。

遵守toString约定并不像遵守equals和hashCode的约定那么重要,后两者不遵守会导致无法预料的错误,而前者只是为了让类使用起来更加舒适。对象被传递给println等方法时,toString方法会被自动调用。

实现toString的时候,必须要做出一个很重要的约定:是否在文档中指定返回值的格式,对于值类(value class)也建议这么做。指定格式的好处是,它可以被用作一种标准的、明确的、适合人阅读的对象表示法。这种表示法可以用于输入和输出,以及用在永久的适合于人类阅读的数据对象中,例如XML文档。如果指定了格式, 最好再提供一个相匹配的静态工厂或者构造器,以便程序员可以很容易地在对象和它的字符串表示法之间来回转换。Java平台类库中的许多值类都采用了这种做法,包括BigInteger、BigDecimal和绝大多数的基本类型包装类。

第十一条:谨慎地覆盖clone

Cloneable接口表明这个对象允许克隆。但是这个接口没有clone方法,Object的clone方法是受保护的。如果不借助于反射,就不能仅仅因为一个对象实现了Cloneable,就可以调用clone方法。即使反射调用也可能会失败,因为不能保证该对象一定具有可访问的clone方法。本条目将讨论如何实现一个行为良好的clone方法,并讨论何时适合这样做,同时也简单地讨论了其他的可替换的方法。

Clonable没有包含任何方法,它只是决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的一种极端非典型的用法,也不值得仿效。

Clone方法的通用约定是非常弱的,下面是来自java.lang.Object规范中的约定内容[Java SE6]:
创建和返回该对象的一个拷贝。这个“拷贝“的精确含义取决于该对象的类。一般的含义是,对于任何对象x,下面表达式将会是true

x.clone() != x;
x.clone().getClass() == x.getClass();
x.clone.equals(x);

第十二条:考虑实现Comparable接口

compareTo方法并不是Object的方法,它是Comparable接口中的唯一方法。compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特征,它还是个泛型。实现了Comparable接口的类的实例有内在的排序关系。

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

在下面的说明中,符号sgn表示数学中的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。
  • 实现者必须确保x.compareTo(y) == 0时,所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
  • 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但这非绝对必要。一般说来,任何实现了Comparable接口的类,若违反了这个条件,都应该予以说明。推荐使用这样的说法:“注意:该类具有内在的排序功能,但是与equals不一致。”
  • -