《Effective Java》第3章 对于所有对象都通用的方法

第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方法的诀窍

  1. 使用==操作符检查“参数是否为这个对象的引用”。
  2. 使用instanceof操作符检查“参数是否为正确的类型”。
  3. 把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
  4. 对于该类中的每个‘’关键(significant)”域,检查参数中的域是否与该对象中对应的域相匹配。

    对干既不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象引用域,可以递归地调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。对Float和double域进行特殊的处理是有必要的,因为存在着Float..NaN、-0.0f以及类似的double常量;详细信息请参考Float.equals的文档。

  5. 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的。

关于equals方法的警告:

  1. 覆盖equals时总要覆盖hashCode。
  2. 不要企图让equals方法过于智能。
  3. 不要将equals声明中的Object对象(参数)替换为其他的类型。

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

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

  1. 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
  2. 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
  3. 如果两个对象根据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异常。

时间: 2024-10-08 11:54:38

《Effective Java》第3章 对于所有对象都通用的方法的相关文章

Effective java 第三章对于所有对象都通用的方法(一) 读书笔记

对于所有对象都通用的方法 覆盖equals时请遵守通用约定 类的每个实例本质上都是唯一的. 不关心类是否提供了逻辑相等的测试功能 超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的. 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用. throw new AssertionError() 一般覆盖Object.equals都是值类 但有一种值类不需要覆盖equals方法,即实例受控,确保每个值至多只存在一个对象的类.如枚举 覆盖equals方法,通用约定. 自

Effective Java读书笔记(3对于所有对象都通用的方法)

3.1 覆盖equals时请遵守通用约定 什么时候应该覆盖Object.equals()方法呢? 如果类具有自己特有的"逻辑相等"概念(不同于对象等同的概念),而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法. Object.equals()方法具有自反性.对称性.传递性.一致性和与null比较返回false的特点. 实现高质量equals方法的诀窍: (1)使用==操作符检查"参数是否为这个对象的引用".如果是,则返回true,这

effective java-读书笔记-第三章 对于所有对象都通用的方法

个人博客同步发布:effective java-读书笔记-第三章 对于所有对象都通用的方法 第三章 对于所有对象都通用的方法 所有非final方法(equals.hashCode.toString.clone.finalize)都有明确的通用约定,因为它们被设计成是要被覆盖的,如果不遵守,基于散列的集合(HashMap.HashSet.HashTable)可能无法结合该类一起运作. 第8条 覆盖equals时请遵守通用约定 覆盖equals规范: 自反性(reflexive).对于任何非null

[Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第八 ---- ?条

这一章主要讲解Object类中的方法, Object类是所有类的父类,所以它的方法也称得上是所有对象都通用的方法 第八条 覆盖equals时需要遵守的约定 Object中的equals实现,就是直接对对象进行相等的比较: public boolean equals(Object obj) { return (this == obj); } 那么什么时候需要覆盖equals呢? 当你的类有自己的逻辑相等,而不是对象相等时,应该自己实现equals,比如Date和Interger,他们的相等比较不仅

Effective Java读书笔记——第三章 对于所有对象都通用的方法

第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它所有的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定. 首先看看equals方法: 若满足下面的这些情况中的某一个,您可以直接使用Object类中的equals方法而不用覆盖: 类的每个实例本质上是唯一的.对于那些代表实例而不是值的类来说可以不用覆盖equals方法.比如Thread类.因为每一个Thread类的实例都表示一个线程,这与Thread某

Effective Java读书笔记——第三章 对于全部对象都通用的方法

第8条:覆盖equals时请遵守通用的约定 设计Object类的目的就是用来覆盖的,它全部的非final方法都是用来被覆盖的(equals.hashcode.clone.finalize)都有通用约定. 首先看看equals方法: 若满足以下的这些情况中的某一个,您能够直接使用Object类中的equals方法而不用覆盖: 类的每个实例本质上是唯一的.对于那些代表实例而不是值的类来说能够不用覆盖equals方法.比方Thread类.由于每个Thread类的实例都表示一个线程,这与Thread某些

[Effective Java 读书笔记] 第三章 对所有对象都通用的方法 第十---十一条

第十条 始终覆盖toString() toString的实现可以使类使用起来更加舒适,在执行println等方法时打印出定制信息. 一单实现了自己的toString,指定输出的固定格式,在方法的文档说明中应该做好注释说明! 第十一条 谨慎覆盖clone

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

前言: 读这本书第1条规则的时候就感觉到这是一本很好的书,可以把我们的Java功底提升一个档次,我还是比较推荐的.这里我主要就关于覆盖equals.hashCode和toString方法来做一个笔记总结,希望能够与君共勉. 概述: 这一章主要是说明一些对于所有对象都通用的方法.我们知道Java的多态是其特色之一,而多态的体现方式中就有一种方式叫做"重写".这些概念性的东西我想在大学我们学习Java的初期,老师就会如数家珍一样地灌输给我们,不过,在那个时候有多少人真的了解了什么是重载,什

effective java读书笔记——对于所有对象都通用的方法

Java中的所有类都继承自Object类,Object类中有许多通用的方法,这一章要讨论的是:对于Object类中的通用方法,我们的类要不要继承,以及继承时需要注意的事项. 第1条:equals(),覆盖时请遵守通用约定 首先看一下不需要覆盖的情况: 1.类的每个实例本质上是唯一的.(比如Static的,单例的等等),这样不需要特意覆盖equals方法,用Object类的equals()方法就足够了 2.不关心类是否实现了“逻辑相等”的测试功能.我们用equals的目的就是判断两个对象是否是“逻