effective java(对于所有对象都通用的方法)

1、覆盖equals时请遵守通用的约定

一、覆盖equals方法看起来似乎很简单,但是许多覆盖方式会导致错误,并且导致严重的后果,最容易避免这类问题的办法就是不覆盖equals方法,在这种情况下,类的每个实例都只于自身相等。如果满足以下任意一个条件,这就正是我们期望的结果:

a、类的每个实例本质上都是唯一的。(对于代表活动的实体而不是值的类来说确实如此,例如Thread。)

b、不关系类是否提供了“逻辑相等”的测试功能。

c、超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。

d、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。(在这种情况下,无疑是应该覆盖equals方法的,以防它被意外调用:

@Override publicl boolean equals(Object o) {

throw new AssertionError(); // Method is never called

}

)

二、如果类具有自己特有的“逻辑相等”,而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。这通常是“值类”的情形。

有一中“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型就属于这种类。

三、覆盖equlas时遵守的通用约定:

》自反性。对任何非null的引用值x,x.equals(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)就会一致的返回true,或者一致的返回false。

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

四、写出高质量equals方法的诀窍:

a、使用==操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这只不过是一种性能优化,如果比较操作符有可能很昂贵,就值得这么做。

b、使用instanceof操作符检查“参数是否为正确的类型”。

c、把参数转化成正确的类型。

d、对于该类中的每个“关键域”,检查参数中的域是否与该对象中对应的域相匹配。

对于即不是float也不是double类型的基本类型域,可以使用==操作符进行比较;对于对象域,可以递归调用equals方法;对于float域,可以使用Float.compare方法;对于double域,则使用Double.compare。如:Float中存在Float.NaN、0.0f。

有些对象引用域包含null可能是合法,为了避免NullPointException

(field == null ? o.field == null : field.equals(o.field))

如果field和o.field通常是相同的对象引用,下面会更快

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

域的比较顺序可能会影响到equals方法的性能,为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。

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

告诫:

a、覆盖equals时总要覆盖hashCode。

b、不要企图让equals方法过于只能。

c、不要equals声明中的Object对象替换为其他类型。


2、覆盖equals时总要覆盖hashCode

equals约定:

a、在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么多次对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。

b、如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生相同的整数结果。

c、如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的结果,但是程序员应该知道,给不相等的对象产生截然不同的结果,有可能提高散列的性能。

hashCode规则:

a、把某个非零的常量值,比如17,保存在一个名为result的int类型的变量中。

b、对于对象中的每个关键域f(指equals方法中涉及的每个域),完成一下步骤:

I.为该域计算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)。然后按照步骤b.I.iii,为得到的long类型值计算散列值。

vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方法来比较这个域,则同样的为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后针对这个范式调用hashCode。如果这个域为null,则返回0(或其他某个常数,但通常是0)。

vii.如果该域是一个数组,则把每一个元素当做单独的域来处理。如果数组域中的每个元素都很重要,可以利用jdk1.5中增加的Arrays.hashCode方法。

II.按照下面的公式,把步骤b.I中计算得到的散列码合并到result中:

result = 31 * result + c;

III.返回result。

3、始终要覆盖toString

默认的toString方法返回的是:类名 + @ + 散列码的无符号十六进制表示法。

toString通用约定:返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”,所有的子类都应该覆盖这个方法。

4、谨慎地覆盖clone

Cloneable接口的目的是作为对象的一个mixin接口,表明这样的对象允许克隆。

Clone方法的通用预定是非常弱的,下面是来自java.lang.Object规范中的约定内容:

创建和返回对象的一个拷贝,这个“拷贝”的精确含义取决于该对象的类。一般的含义是,对于任何对象x,表达式

x.clone() != x

将会是true,并且,表达式

x.clone().getClass() == x.getClass()

将会是true,但这都不是绝对的要求,虽然通常情况下,表达式

x.clone().equals(x)

将会是true,但是,这也不是一个绝对的要求。拷贝对象往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程没有调用构造器。

这个约定存在几个问题。“不调用构造器”的规定太强硬了。行为良好的clone方法可以调用构造器来创建对象,构造之后再复制内部数据。如果这个类是final的,clone甚至可能会返回一个由构造器创建的对象。

@Override public PhoneNumber clone(){

try{

return (PhoneNumber) super.clone();

} catch {

throw new AssertionError(); // Can't happen

}

}

clone方法返回的是PhoneNumber,而不是Object,从java1.5开始,这么做是合法的,在1.5中引入了协变返回类型作为泛型。

这里体现了一条通则:永远不要让客户去做任何类库能够替客户完成的事情。

如果对象中包含的域引用了可变的对象,使用上述这种简单的clone实现可能会导致灾难性的后果。例如:

public class Stack {

private Object[] elements;

}

假设你希望把这个类做成可克隆的。如果它的clone方法仅仅返回super.clone(),它的elements域将引用与原始Stack实例相同的数组。修改原始的实例会破坏克隆对象中的约束,反之亦然。

@Override public Stack clone(){

try{

Stack result = (Stack) super.clone();

result.elements = elements.clone();

return result;

} catch {

throw new AssertionError(); 

}

}

我们不一定要将element.clone()的结果转换成Object[]。自JAVA 1.5起,在数组上调用clone返回的数组,其编译时类型与被克隆数组的类型相同。

还要注意,如果elements域如果是final的,上述方案就不能正常工作。

递归的调用clone有时还不够。

public class HashTable implements Cloneable {

private Entry[] buckets = ...;

private static class Entry {

final Object key;

Object value;

Entry next;

}

}

假如想Stack那样:

@Override public HashTable clone() {

try {

HashTable result = (HashTable) super.clone();

result.buckets = buckets.clone();

return result;

} catcht (CloneNotSupportedException e) {

throw new AssertionError();

}

}

虽然被克隆对象有他自己的散列数组,但是,这个数组引用的链表与原始对象是一样的,从而很容易引起克隆对象和原始对象中不确定的行为。为了修改这个问题,必须单独地拷贝并组成每个桶的链表。

public class HashTable implements Cloneable {

private Entry[] buckets = ...;

private static class Entry {

final Object key;

Object value;

Entry next;


Entry deepCopy(){

return new Entry(key, value, next == null ? null : next.deepCopy());

}

}


@Override public HashTable clone(){

try{

HashTable result = (HashTable) super.clone();

result.buckets = new Entry[buckets.length];

for(int i = 0; i < buckets.length; i++)

if (buckets[i] != null)

result.buckets[i] = buckets[i].deepCopy();

return reuslt;

}catch(CloneNotSupportedException) {

throw new AssertionError():

}

}

}

如果散列桶太长,为防止栈溢出,修改deepCopy

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;

}

@Override public PhoneNumber clone(){

try{

return (PhoneNumber) super.clone();

} catch {

throw new AssertionError(); // Can't happen

}

}.

克隆复杂对象的最后一种办法是,先调用super.clone,然后把结果对象中所有域都设置成它们的空白状态,然后调用高层的方法来重新产生对象的状态。

这种做法往往会产生一个简单、合理且相当优美的clone方法,但是它运行起来通常没有“直接操作对象及其克隆对象的内部状态的clone方法”快。

5、考虑实现Comparable接口

比较整数型基本类型的域,可以使用“>”和“<”,float、double使用compare方法。

如果一个类有多个关键域,那么,按什么样的顺序来比较这些域是非常关键的。你必须从最关键的域开始,逐步进行。

当用“-”判断结果是>0、<0、==0时,当心溢出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值