读书人

Effective Java:Ch3_Methods:Item8_

发布时间: 2013-03-22 09:49:50 作者: rapoo

Effective Java:Ch3_Methods:Item8_重写equals方法时遵循通用约定

虽然Object类是一个具体类,但它主要还是用于扩展。因为其所有nonfinal方法(equals、hashCode、toString、clone以及finalize)都是为重写设计的,所以这些方法都有显式的通用约定。任何重写这些方法的类都有责任遵循这些通用约定;若未做到这一点,则其他依赖于这些约定的类就不能与之一起正常运作。

本章讲述何时如何覆盖Object中的nonfinal方法。由于Item7已逃离了finalize,所以本章不再赘述。Comparable.compareTo虽然不是Object中的方法,但是由于它有类似的特性,所以也在本章讨论。


重写equals方法看起来很简单,但是许多重写方式会导致错误,并且后果很严重。如果类的实例只与其自身equals,那么避免这种问题的最简单办法就是不去重写equals方法。只要满足以下任一条件,就可以这样做。

类的每个实例本质上都是唯一的。【例】例如Thread这样的表示活动实体,而非表示值的类。Object类提供的equals实现对于这些类来说恰好拥有正确的行为。不关心类是否提供“逻辑相等”的测试功能。【例】例如java.util.Random本可以重写equals来检查两个Random实例是否生产了同样的随机数字序列,但是设计者不认为客户端会需要这样的功能。在这些情况下,从Object继承来的equals实现就已经足够了。父类已经重写equals,并且父类的行为对于子类也是合适的。【例】例如大多数Set的实现类都从AbstractSet继承了equals方法,List的实现类从AbstractList继承了equals方法,Map的实现类从AbstractMap继承了equals方法。类是私有的,或包级私有,并且确信其equals方法永远不会被调用。Arguably,其equals方法是应该重写成如下形式,以防止被意外调用:

(field == o.field || (field != null && field.equals(o.field)))

对于某些类,例如上面的CaseInsensitiveString,域的比较要比简单的相等性测试要复杂得多。如果是这种情况,你可能会希望保存该域的一个标准形式(canonical form),equals方法在这些标准形式上进行低开销的精确比较,而不是进行高开销的非精确比较。这种方式最适合于不可变类(Item15);如果对象改变,需要连带更新其标准形式

域比较的顺序可能会影响equals方法的性能。为了获得最佳性能,应该首先比较哪些最可能不一致的域、开销最小的域。不应该比较哪些并非对象逻辑状态的域,例如同步操作的Lock域。不应该比较冗余域,这些域可通过关键域计算而得,但是这样做可能提高equals方法的性能。如果冗余域代表了对整个对象的综述,那么对比这些域就能够节省比较开销。【例】例如,假设有个Polygon类,并缓存了面积域,如果两个Polygon的面积不等,那就不用再去比较他们的边和顶点了。


5)当你完成equals方法后,问自己三个问题:它是否是对称的、传递的、一致的?并且不仅是自问,还要编写单元测试来检验这些特性。如果不通过,找出原因,据此来修改equals方法。当然,equals还需要满徐其他两个特性(自反性、非空性),不过这两种特性通常会自动满足。

Item9中的PhoneNumber.equals就是根据上述诀窍编写的。下面是最后的一些说明:

当重写equals时必须重写hashCode(Item9)不要让equals过度聪明。如果只简单地测试域是否相等,则不难满足equals约定;而当过度地寻求等价关系,则容易陷入麻烦之中。例如File类不应该将指向同一文件的符号链接当做相等对象来看待。所幸File类没有这样做。在equals方法声明中不要将Object替换为其他类型。为了防止这种情况,应该在每次重写equals时都是用@Override。




读书人网 >编程

热点推荐