第八条 改写equals时总是要改写hashCode
每个改写了equals方法的类中,你必须也要改写hashCode方法。
hashCode约定的内容:
1. 在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,则对该对象调用hashCode方法多次,它必须返回始终如一的同一个整数。
2. 如果两个对象根据equals(Object)方法是相等的,这两个对象所产生的hashCode也相等。
3. 如果两个对象根据equals(Object)方法是不相等的,hashCode最好不等。
生成hashCode的处方:
1. 把某个非零常数值,保存在一个叫result的int变量中。
2. 对对象中每一个关键域f (指equals方法中考虑的每一个域),完成:
a. 为该域计算int类型的散列码c:
i. 如果该域是boolean类型,则计算(f ? 0 : 1)。
ii. byte、char、short or int类型,则计算(int)f。
iii. long: 计算(int) ( f ^ (f >>> 32) )。
iv. float: 计算Float.floatToIntBits(f)。
v. double:计算 Double.doubleToIntBits(f)得到一个long类型的值,然后按步骤iii 对该long型进行散列计算。
vi. 对象引用:若该类的equals通过递归调用equals来比较这个域,则同样对这个域递归调用 hashCode。如果要求更复杂的比较,则为这个域计算一个“规范表示”,然后对范式表示调用hashCode。如果这个值为null,则返回0。
vii. 数组:吧每个元素当作单独的域来处理。然后按b中做法把散列值组合起来。
b. 把步骤a中计算得到的散列码c组合到result中:
result = 37*result + c (选用37是因为它是一个奇素数)
3. 返回result。
4. 写完hashCode检查是否相等的实例具有相等的散列码。
如果一个类是非可变的,并且计算散列码的代价也很大,则考虑把散列缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果觉得此类的大多数对象会被用做散列键,那么应该在实例被创建的时候就计算散列码。否则,可选择“迟缓初始化”散列码,一直到hashCode被第一次调用的时候才初始化。
private
volatile
iint hashCode = 0 ;
public
int
hashCode(){
if
(hashCode == 0){
int
result = 17;
result = 37*result + areaCode;
result = 37*result + exchange;
result = 37*result + extension;
hashCode = result;
}
return
hashCode;
}
不要试图从散列码计算中排除掉一个对象的关键部分以提高性能。
第九条
:总是要改写toString
java.lang.Object提供的toString方法,一般返回类的名字@散列码的无符号十六进制表示。toString的约定建议所有的子类都改写这个方法。
提供一个好的toString
实现,可以使一个类用起来更加愉快。
在实际应用中,toString
方法应该返回对象中包含的所有令人感兴趣的信息。
实现toString的时候,必须决定是否在文档中制定返回值的格式。对于值类,推荐这样做。好处是可以被用做一种标准的、无二义性的、适合人阅读的对象表达形式。一个好的做法是同时提供一个相匹配的String构造函数或者静态工厂,这样可以很容易地在对象和它的字符串表示之间来回转换。
指定格式的不足:如果这类已经被广泛使用了,则一旦指定了格式,必须坚持这种格式。
无论是否决定指定格式,都应该在文档中明确地表明你的意图。
最好为
toString
返回值中包含的所有信息,提供一种变成访问途径。
第十条
:谨慎地改写clone
Cloneable接口的目的是作为对象的一个mixin 接口,表明这样的对象允许克隆。可它并没成功达到这个目的,因为Object的clone方法是被保护的,如果不借助于映像机制(reflection 35条),则不能仅仅因为一个对象实现了Cloneable就可以调用clone方法。即使映像调用也可能失败,因为不能保证该对象一定具有可访问的clone方法。
Cloneable决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,则Object的clone方法返回该对象的逐域拷贝,否则抛出CloneNotSupportedException异常。这种用法不值得仿效。
为了实现一种语言之前的机制:无须调用构造函数就可以创建一个对象。Clone方法需要遵守下面的约定:
创建和返回该对象的一个拷贝,这里的“拷贝”的精确含义取决于该对象的类。
x.clone() != x ; x.clone().getClass() == x.getClass() ; x.clone().equals(x) == true
但这些都不是绝对要求。
如果改写了一个非final
类的clone
方法,则应该返回一个通过调用super.clone
二得到的对象。这种机制大致类似于自动的构造函数链,不过它不是强制要求的。
对于实现了Cloneable的类,我们总期望它也提供一个功能适当的公有clone方法。但通常,除非该类的所有超类都提供了一个行为良好的clone实现,否则是不可能的。
假设你的超类都提供了行为良好的clone方法,你从super.clone()中得到的对象可能会接近于最终要返回的对象,也可能相去甚远,这取决于类的本质。从超类的角度看,这个对象将是原始对象功能完整的克隆。在这个类中声明的域将等同于被克隆对象中相应的域值。如果每个域包含原语类型值或者非可变对象的引用,返回的对象可能是你需要的对象。然而,如果对象中包含的域引用了可变的对象,或者代表序列号活其他唯一ID值的域,或者代表对象创建时间的域,那么使用super.clone可能会导致灾难性后果。
clone
方法是另一个构造函数;你必须确保它不会伤害到原始的对象,并且正确地建立起被克隆对象中的约束关系。
public
Object clone()
throws
CloneNotSupportedException{
Stack result = (Stack)
super
.clone();
result.
elements
= (Object[])
elements
.clone(); //
递归地调用
clone
return
result;
}
clone
结构与指向可变对象的
final
域的正常用法是不兼容的。
这里如果
elements
是
final
的,则这种方案不能正常工作。除非在原始对象和克隆对象之间可以安全地共享此可变对象。
如果你在为一个散列表编写clone方法,它的内部数据是由一个散列桶数组组成,每一个散列都指向“健-值”对链表的第一个条目。为了实现这个类的clone,必须每个组成桶的链表单独地拷贝,深度拷贝:
Entry deepCopy(){
Entry result =
new
Entry(
key
,
value
,
next
);
for
(Entry p = result; p.
next
!=
null
;p=p.
next
)
p.
next
=
new
Entry(p.
next
.
key
,p.
next
.
value
,p.
next
.
next
);
return
result;
}
克隆复杂对象的最后一个方法是,先调用super.clone,然后把结果对象中的所有域都设置到它们的空白状态,然后调用高层的方法来重新产生对象的状态,比如 HashTable中的put(key,value)的方法。这种做法运行起来没有直接操作对象和其克隆的内部状态的clone方法快。
如同构造函数一样,clone方法不应该在构造过程中,调用新对象中任何非final方法。因此,上一段的put(key,value)要么是final的,要么是私有的。
如果一个可扩展类改写了clone方法,那么改写版本的clone方法应包含
CloneNotSupportedException
异常。这样做可以使子类通过提供下面的
clone
方法,选择温和地放弃克隆能力:
public
final
Object clone()
throws
CloneNotSupportedException{
throw
new
CloneNotSupportedException();
}
另一个实现对象拷贝的好办法是提供一个拷贝构造函数。
public Yum(Yum yum);
public static Yum newInstance(Yum yum);
它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未良好文档化的规范;它们不会与final域的正常使用发生冲突;它们不会要求客户捕获不必要的被检查异常;它们为客户提供了一个静态类型化的对象。