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

Object类默认为所有类的基类,其虽然为一个具体的类,但是它的设计主要是为了扩展,而它的所有非final的方法(equals, hashCode, toString, clone和finalize)都有明确的通用约定, 如果在自定义类时要重写这些方法,需要注意主动遵守这些约定,不然一些依赖于这些约定的类(如HashMap和HashSet)将无法正确工作。


通用概念

值类:表示值的类,也就是说这个类的对象的相等是说其代表的值是一样的,并非仅仅对象的相等


1.覆盖equals方法时切记遵守通用约定【Item 8】

1)避免问题发生的最好的方式是不覆盖,那么如下情况可以不用覆盖equals方法

a)类的每个实例本质上都是唯一的,Object类的equals方法实现对于这些类来说是正确的行为

b)不关心类是否提供了“逻辑相等”的测试功能,也就是说在这个类在使用过程中并不关心两个对象的值在逻辑上相等的功能

c)父类已经覆盖了equals方法,从父类继承过来的行为对于子类也是合适的。例如大多数Set/List/Map均从AbstractSet/AbstractLst/AbstractMap继承equals的实现

d)值类中的设计为单例的类,以及枚举类型

2)equals方法实现了等价关系,因此其在被覆盖是需要保持等价的特性,数学中等价需要满足如下条件:

a)自反性,对于任何非null的值x,x.equals(x)返回必须为true

b)对称性,x.equals(y)返回true,则y.equals(x)也必须为true,这依赖于两个对象对应类的equals方法的实现,双方相等的口径需要是一致的

c)传递性,x.equals(y)返回true,y.equals(z)返回为true, 则x.equals(z)也必须为true

d)一致性,只要equals中用来比较的值没有改变,多次调用equals方法返回值应该一致的,避免使equals方法依赖于不可靠的资源

e)对于任何非null的引用值x,x.equals(null)必须返回false

3)高质量的equals方法实现诀窍

a)使用==操作符检查“参数是否这个对象的引用”,若是,直接返回true

b)使用instanceof操作符检查“参数是否为正确的类型”,可以是本类型或者实现统一接口的类型

c)把参数转换为正确的类型,由于前一步已经进行了类型判定,因此此处可以直接进行类型转换

d)对于该类中的每个“关键”域,检查参数对象中的域是否与该对象中对应的域相匹配,这里可以从类中最关键的域开始检查,一旦不等直接返回false,对于域值的判定方式分别为:基本类型(float,double除外)使用==操作符进行比较;对于对象引用域可以通过递归调用equals方法进行;对于float使用Float.compare,对于double使用Double.compare;数组域按照上述比较规则应用于每一个元素,如果数组上每个元素都很重要,可以使用1.5版本新加的Arrays.equals方法

对于域值的比较,为了性能考虑,建议先比较最可能不一致的域,尽可能减少不必要code的执行

4)零碎的注意点

a)我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定,这个时候可以通过组合来代替继承。

b)可以在一个抽象类的子类中增加新的值组件,而不违反equals约定

c)在重写完equals方法之后一定要针对自反性,对称性,传递性和一致性写自测code进行验证,如果不满足要及时调整,以免为debug增加不必要的负担

d)重写equals方法时总要重写hashCode方法

e)不要企图equals方法过于智能,如果将该方法设计得过于智能,可能增加不必要的负担,由于JDK中类好多功能依赖equals,重写的越复杂代码越难维护

f)不要将equals声明中的Object对象替换为其它类型,这点可以通过@Override注释来帮忙保证,因为重写需要成员方法的签名一致(方法名与参数唯一确认签名)

2.重写equals方法时一定记得要重写hashCode方法【Item 9】

1)相等的对象必须有相等的散列码(hash code)

2)一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”

3)散列码的一种计算方式是,通过对象的每个域值做相应的数学计算(加减乘除),最后所有计算结果加起来构成该对象的散列码,通常来讲null的散列码为0

4)散列码的计算需要排除冗余域(可以通过其它域计算出来的域),以及equals方法中比较没有用到的域

5)如果一个类是不可变的,并且计算散列码的开销比较大,那么建议将散列码保存于对象内部,不是每次请求都计算一遍

6)如果你觉得类型的大多数对象会被用作散列键值,那么需要对象被创建时计算散列码,如果不是,则可以延迟hash code的计算到hashCode方法被第一次调用的时候

7)不要尝试从散列码的计算中排除掉一个对象的关键部分来提高性能,否则会本末导致,牺牲程序执行的正确性换来的性能没有任何意义

3.始终要重写toString方法【Item 10】

1)建议所有子类都重写toString方法,toString应该返回对象中包含的所有值得关注的信息

2)toString是debug时获取信息的重要方式,好的toString方法能够使得调试工作事半功倍

3)toString方法可以采用固定格式的实现方式也可以不固定而是,但是一定注意要注释清楚输出格式对应的意义

4)无论是否指定格式,都为toString返回值中包含的所有信息,提供一种编程式的访问途径,这样有需求要获取输出信息中的内容时可以通过getXXX或者对象公开域来获取,不用费劲去解析toString返回内容

4.谨慎的重写clone方法【Item 11】

1)永远不要让客户去做任何类库能够为客户完成的事情,在子类的clone方法中调用super.clone,父类会做一部分的转换工作,不需要子类来进行

2)clone就是另一个构造器,你必须确保它不会伤害到原始对象,并确保正确的创建被克隆对象中的约束条件

3)clone架构与应用可变对象的final域的正常用法是不兼容的,除非在原始对象和克隆对象之间共享该可变对象,否则想要保持类的可克隆性,需要去掉某些域的final属性

4)对于不可变类,支持对象的拷贝并没有太大的意义,因为被拷贝的对象与原始对象病没有实质的不同

5)在实现clone的时候一定要拷贝的深度,以及拷贝的同步性控制,以及原始对象与拷贝对象相互之间的影响

5.考虑实现Comparable接口【Item 12】

1)类实现了Comparable接口,就表明它的实例具有内在的排序关系

2)只要类实现了Comparable接口,JDK类库中依赖该接口的诸多功能你都可以使用,并且Java类库中的所有值类都实现了该接口

3)如果你在设计一个类时其需要排序功能,那么你应该实现Comparable接口

4)compareTo方法的返回值通过值的正值(大于),负值(小于)和零(等于)来代表比较结果

5)compareTo方法也应该保持自反性,对称性,传递性

6)强烈建议(x.compareTo(y)==0)==(x.equals(y)),也即是在等于上建议同equals保持一致

7)在跨越不同类的时候,compareTo可以不做比较,直接排除ClassCastException

8)如果破坏compareTo的约定,则就破坏了依赖compareTo的类似TreeSet,TreeMap的功能

9)有序集合依赖于CompareTo方法,普通集合(Collection,Set或Map)依赖于equals方法,如果你的类没有遵守这两个方法的通用约定,那么在包含这个类的对象的集合可能无法遵守相应集合的通用约定,也就可能使得集合类的工作异常,也就是这些方法的重写决定着类库中集合的工作方式,需谨慎

10)Comparable接口是参数化的,而且compareTo方法是静态的类型,即compareTo中的参数类型同Comparable接口的参数化定义的类型一致,因此不需要进行参数的类型转换

11)如果一个域没有实现Comparable接口,或者你需要使用一个非标准的排序关系,那么可以使用一个显示的Comparator来代替,使用已有的或者自定义的Comparator

12)多个域的比较类似&&逻辑表达式的截断功能,只要某个域的比较出现了非零值即可返回,后续的域不需要在进行比较


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值