《Effective Java》第三章:对于所有对象都通用的方法
对于所有对象都通用的方法
类在继承时,它的所有非final
方法都用明确得通用约定,他们都是被设计为要被覆盖的,对于任何一个类,在覆盖这些方法的时候,都需要遵循这些通用的约定,如果做不到这一点,其他依赖于这些约定的类将无法结合该类一起工作。
第8条:覆盖equals时请遵守通用约定
我们都知道
equals
方法称被用于判断两个对象是否相等(除地址外),那你知道该方法应该被如何设计呢?
equals
方法需要遵循的约定:
- 自反性:对于任何非
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)
返回的值一定相等。 - 对于任何非
null
的引用值x
,x.equals(null)
一定为false
,关于这一条,不必要在equals
方法是使用null
的判断。
对于这些约束,编写equals
有下面几个诀窍:
- 使用
==
操作符检查“参数是否为这个对象的引用”(就是判断对象是否地址相等) - 使用
instanceof
操作符检查“参数是否为正确的类型”(判断对比的参数是否为本类或者超类,同时可以判断null值) - 把参数转为正确的类型(强转了之后才能进行下一步的具体值的判断^_^)
- 对于该类中的每个关键域,检查参数中的域是否与该对象中对应域相匹配(说实话,这个没怎么看懂-_-)
- 当编写完
equals
方法之后、应该问自己三个问题:他们是否是对称的、传递的、一致性的(这不是常见的几个约束嘛?最好是需要满足,同时最好还得有单元测试去确保)
一般equals
一般通用的写法是:1
2
3
4
5
6
7
8
9
10
11
12
13@Override
public boolean equals(Object o)//注意这里传的是Object类型,不是具体的类型,不然就是重载了,而不是重写
{
//这一步很重要,一个是可以判断当前o是否为本类型或者他的超类,另外这里如果o为null的话会直接返回false
if(!(o instanceof MyType))
{
return false;
}
MyType mt=(MyType)0;//将obj转为本类型之后再操作
//Todo 根据自己业务写的实际的判断情况、如果涉及子类超类的可以使用递归调用,完了之后进行布尔值的返回
}
建议:如果你无法确保你自己写的
equals
满足上述约束,那就不要去重写这个equals
方法的,因为超类的该方法一般也是适用于子类的(懒人模式)。
第9条:覆盖equals时总要覆盖hashCode
地球人都知道调用
hashCode
方法可以得到该对象的散列值(Hash值)
先来看一下hashCode
方法的相关通用约束:
- 在程序执行时,只要
equals
方法所用到的对象没有被修改,得到的hashCode
的值一定还是原来的值。 - 相等的对象必须具有相等散列码。
- 不相等的对象他们的散列码一定不相等。
所以,为了不违反第2条约束,在覆盖了equals
方法之后一定要覆盖hashCode
。
一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”,散列函数应该把集合中不相等的实例均匀的分不到所有的可能额散列值上,如果想完全达到这样的效果是非常困难的,但是可以使用相对接近的理想情形并不困难(具体散列步骤请移步原文去参考-_-)
第10条:始终要覆盖toString
我们在打印一个类时,是不是会经常看到一个类的名称,以及一个@符号,接着是散列码的无符号十六进制数,列如:“PhoneNumber
@163b91”,这样的打印信息对于开发者来说几乎是无意义的,所以“建议所有的子类都要覆盖这个toString
”方法。
toString
在编写时没有那么多的通用约束,但是尽量能表达该类的信息,当然也可以编写含有你特征格式的toString
返回值,比如XML
格式,这种方式需要在注释里面特别说明。
PS:建议是好的,但是每写一个类都去覆盖他的
toString
方法会不会太累?个人觉得如果没有用到println
,printf
之类的方法不去覆盖也是ok的^_^
第11条:谨慎地覆盖clone
Cloneable
接口并不含任何方法,但是实现它的时候Object
的clone
方法就会返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException
异常
创建和返回对象的拷贝需要满足(这几点其实在Object.clone()
的源码中):
x.clone()!=x
(拷贝返回的东西不能用原来的地址啊~^_^)x.clone().getClass==x.getClass()
x.clone().equals(x)
(拷贝了之后当然两个对象时相等的~)
注意,在java1.5以后的版本中
clone()
引入了协变返回类型,额可以直接直接支持指定类的返回,不必必须返回Object
clone()
的原则就是必须确保不会伤害到原始的对象,并确保正确地创建被克隆中的约束条件。
实现克隆最基本的方法是:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class CloneTest implements Cloneable{
public int[] elements=new int[10];
/**
* 进行数组的打印
*/
public void print()
{
System.out.println("\r\nprint");
for(int i=0;i<this.elements.length;i++)
{
System.out.print(elements[i]+"#");
}
}
@Override
public CloneTest clone()
{
CloneTest ret=null;
try
{
ret=(CloneTest)super.clone();//调用超类的clone
}catch(CloneNotSupportedException cs)
{
cs.printStackTrace();
}
return ret;
}
}
但是他会破坏原有对象:
1 | CloneTest t1=new CloneTest(); |
返回的是:
print
4#6#0#0#0#0#0#0#0#0#
print
4#6#0#0#0#0#0#0#0#0#
这是因为该方法仅仅只是调用了超类的克隆方法,完了之后克隆出来的对象的elements
元素还是与原对象一样。
所以一般在编写clone()
之后,再在克隆出的对象上添加自己当前类所需要的元素,上述可以这么修改:
1 | @Override |
在先前的测试代码之后得到的是
print
4#5#0#0#0#0#0#0#0#0#
print
4#6#0#0#0#0#0#0#0#0#
简而言之,所有实现了Cloneable
的类都应该调用一个共有的方法覆盖clone
,此公有的方法首先调用super.clone()
,然后修正任何需要修正的域(其实就是对当前自己类上的元素单独clone()
)。
最后,关于clone()
方法有以下几个注意点:
- 将出现递归
clone()
的时候尽量使用循环迭代来代替。 - 如果是线程安全的类要实现
clone()
,那些这个clone()
也必须进行同步。 - 关于自身域的修正如果是遇到
final
类型的,那么这两者是不兼容的。 - 另一个实现对象拷贝的方式就是提供一个拷贝构造器,该构造器比
clone()
方法的一个优势就是它可以传参数。 - 当前类没有继承
Cloneable
接口时,如果掉clone()
方法里面调用了super.clone()
方法就会抛出CloneNotSupportedException
异常。
第12条:考虑实现Comparable接口
compareTo
方法是Compareable
接口中唯一的方法,它不但可以进行简单的等同性比较外,而且允许执行顺序比较。
其实Comparable
与equals
的约定是极其像的:
- 自反性
- 对称性
- 传递性
与equals
不同的是,在跨域不同类的时候,compareTo
可以不做比较,如果两个待比较的对象时不同引用的对象,compareTo
可以抛出ClassCastExcetpion
异常。
compareTo
方法在执行时,如果小于被比值,返回-1(也可以其他负数),或者等于比较值,此时返回0,否则都是大于0的情况就返回false。
关于在有多个关键域都需要进行对比时,可以使用减法差值来减少编码的代码量。