尽管Object
是一个具体类,但设计它主要是为了扩展。它的所有非final方法(equals
、hashCode
、toString
、clone
和finalize
)都有明确的通用约定,因为它们被设计成可被覆盖(override)的。任何一个类,在覆盖这些方法的时候,都有责任遵守这些约定。如果不能做到这一点,其它依赖于这些约定的类,比如HashMap
和HashSet
,就无法结合该类一起正常运作。
本章将讨论何时以及如何覆盖这些非final的Object方法。本章不讨论finalize方法,因为在第一章的 Item8 里已经讨论过。而
Comparable.compareTo
虽然不是Object方法,但也将对它进行讨论,因为它具有类似的特征。
覆盖equals方法看似简单,但是很多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的方法就是不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。
如果满足以下任何一个条件,就无需覆盖:
根据以上的诀窍构建equals方法的具体例子如下:
public final class PhoneNumber{
private final short areaCode, prefix, lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber){
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
@Override public boolean equals(Object o){
if(o == this){
return true;
}
if(!o instanceof PhoneNumber){
return false;
}
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;.
}
}
下面是最后的一些忠告:
public boolean equals(MyClass o){
//
}
这样写并没有覆盖(override)equals方法,因为Object类的equals方法需要的参数类型就是Object,如果改成其它类型,则相当于重载(overload)了该方法,而不是覆盖。
IDE也有工具可以自动生成equals方法和hashCode方法,通常IDE自动生成的比程序员手工实现的可靠性高一些,因为IDE不会犯粗心的错误。
总而言之,不要轻易覆盖equals方法,除非迫不得已。在许多情况下,从Object处继承的实现正是你想要的。如果要覆盖,一定要比较这个类的所有关键字段,并检查是否遵守了前面提到的规范。
在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。 如果不这么做,就会违反hashcode的通用规定,而导致该类无法结合基于散列的集合一起正常运作,这类集合包括HashMap
和HashSet
。
以下是类的hashCode方法的一些规范:
在应用程序执行期间,只要对象的equals方法比较操作所用到的字段信息没有被修改,那么对同一个对象多次调用hashCode方法,必须返回同一个值。同一个应用程序的一次执行和另一次执行过程中,调用hashCode方法所返回的值可以不一致。
如果两个对象根据equals方法比较是相等的,那么调用这两个对象的hashCode方法必须产生相同的结果。
如果两个对象根据equals方法比较是不相等的,那么调用这两个对象的hashCode方法,不要求必须产生不同的结果。
但是程序员应该知道,给不相等的对象产生不同的结果,能提高散列表的性能。不相等的对象的hashCode结果应该尽可能地不相等。
先看一个糟糕的hashCode方法的例子:
@Override public int hashCode(){
return 43;
}
当然,这是一个合法的hashCode方法,但是它对所有的对象都返回同一个hashCode值。如果把这些对象存到散列表中,会使散列表退化成链表。它使得本该线性时间运行的程序变成了平方级时间运行,使性能大打折扣,甚至会导致程序无法正常工作。
一个好的hashCode方法应该遵循上述第三条规范,尽可能为不相等的对象产生不相等的散列码。理想情况下,hashCode方法应该把集合中不相等的对象均匀分布到所有可能的值上。要达到这种理想情况是非常困难的,但我们可以实现相对接近这种理想情况。下面给出一个简单的实现方法:
声明一个int变量并命名为result,将它初始化为对象中的一个关键域的散列码。(关键域是指影响equals方法比较的域)
对剩下的每个关键域都执行以下操作:
2.1 计算该域f
的散列码c
Type.hashCode(f)
方法计算Arrays.hashCode
方法2.2 按照下面的公式,把2.1中计算得到的散列码合并到result中:
result = 31 * result + c;
在散列码的计算过程中,可以把衍生域排除在外。也就是说,如果一个域的值可以根据其他域的值计算出来,就可以把这样的域排除在外。另外,必须排除在equals比较计算中没有用到的任何域,否则可能违反规范的第二条。
现在我们把上述方法用到Item1中的PhoneNumber类中:
@Override public int hashCode(){
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNumber);
return result;
}
Guava Hash库
虽然前面给出的hashCode实现方法能够获得相当好的散列函数,但它不是最先进的。它的质量堪比Java平台库类的值类型中提供的hashCode方法,这些方法对于绝大多数应用程序而言已经足够。如果执意让hashCode方法尽可能不造成冲突,可以参考 Guava Hash 库 com.google.common.hash.Hashing
Object.hash()方法
Objects
类有一个静态方法 Objects.hash(Object... values)
,它可以接收任意数量的参数并返回一个散列码,用一行代码就可以实现与前面介绍的方法同样的效果。但其运行速度会更慢一些,因为它会引发数组的创建,如果参数中有基本数据类型,还会引发装箱和拆箱。用它来实现前面PhoneNumber类的hashCode方法,如下:
@Override public int hashCode(){
return Objects.hash(lineNumber, prefix, areaCode);
}
缓存hashCode值
如果一个类是不可变的,并且计算hash值的开销也比较大,就可以考虑把hash值缓存在对象内部,而不是每次请求的时候都重新去计算。还是以前面的PhoneNumber类作为例子
prviatre int hashCode;
@Override public int hashCode(){
int result = hashCode;
if(result == 0){
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNumber);
}
return result;
}
虽然Object类提供了toString方法的实现,但它的返回字符串通常不是用户期望看到。它包含类的名称,以及一个@
符号,接着是散列码的无符号十六进制表示法。例如一个PhoneNumber对象的toString结果可能是PhoneNumber@163b91
toString的通用约定指出,它返回的应该是一个简洁的但内容信息丰富,并且易于阅读的表达形式。提供良好的toString方法实现不仅使类用起来更加舒适,也更加便于调试。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。
在静态工具类中编写toString方法是没有意义的。也不要在多少枚举类中编写toString方法,因为Java已经为枚举提供了完好的实现方法。
总而言之,要在每一个可实例化的类中覆盖Object的toString方法,除非已经在父类中这么做了。
一个类若想覆盖clone方法,需要先实现Cloneable
接口。如果它的超类提供了行为良好的clone方法。首先,调用super.clone方法,将得到一个原始对象功能完整的克隆。如果该类的每个域都是基本数据类型的值,或者是指向不可变对象的引用,这种情况就无需再作进一步处理。
以前没的PhoneNumber类为例,它的clone方法应该是这样的:
@Overrideprotected PhoneNumber clone() throws CloneNotSupportedException {
try {
return (PhoneNumber)super.clone();
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
虽然Object的clone方法返回的是Object,但这个clone方法返回的却是PhoneNumber。这么做是合法的,也是符合期望的,因为Java支持协变返回类型。换句话说,覆盖方法的返回类型可以是被覆盖方法的返回类型的子类。
如果对象中的域引用了可变的对象,那么上述拷贝方法可能会引发严重的后果。
比如下面这个Stack类:
class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_SIZE = 16;
public Stack(){
this.elements = new Object[DEFAULT_SIZE];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
如果它的clone方法仅仅返回super.clone(),这样得到的Stack实例,其size域具有正确的值,但它的elements域将引用原始Stack实例相同的数组。对克隆获得的对象的elements域的修改将影响原始对象的值,反之亦然。这当然不是我们想要的结果。
实际上,clone方法就是另一个构造器,必须确保它不会影响到原始对象,并确保被创建后的对象也不会受到原始对象的影响。
为了是Stack类中的clone方法正常工作,它必须拷贝栈的内部信息。最容易的做法就是在elements数组中递归地调用clone方法。如下:
@Overrideprotected
Stack clone() throws CloneNotSupportedException {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
要注意的是如果elements域是final的,上述方案就不能正常工作,因为final域是无法赋新值的。这就造成了一个问题,Cloneable架构与可变引用的final域的正常用法是不兼容的,除非把final修饰符去掉。
简而言之,所有实现了Cloneable接口的类都应该覆盖clone方法,并且是公有方法,它的返回类型为类本身。该方法应该先调用super.clone,然后修正任何需要修正的域。一般情况下,这意味着要拷贝任何包含深层结构的可变对象,并用指向新对象的引用替代原来指向这些对象的引用。
对象拷贝的更好的办法是提供一个拷贝构造器或者拷贝工厂。
拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类,如下:
public Yum(Yum yum){
...
}
拷贝工厂是类似于拷贝构造器的静态工厂:
public static Yum newInstance(Yum yum){
...
}
拷贝构造器的做法,及其静态工厂方法的变形,都被Cloneable/clone方法更具有优势:它们不依赖某一种有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好文档的规范;也不会与final域的正常使用发生冲突;他们不会抛出不必要的异常,也不需要进行类型转换。
既然所有的问题都与Cloneable接口有关,新的接口就不应该扩展这个接口,新的可扩展的类也不应该实现这个接口。
总之,复制功能最好由构造器或者工厂提供。这条规则的绝对例外是数组,最好利用clone方法复制数组。
类实现了Comparable接口,就表明它的实例具有内在的排序关系。对实现了Comparable接口的对象数组进行排序非常简单:
Arrays.sort(a);
每当实现一个对排序敏感的类时,都应该让这个类实现Comparable接口,以便其实例可以轻松地被分类、搜索,以及用在基于比较的集合中。当在compareTo方法中实现比较域值时,应该避免使用 > 和 < 操作符,而应该使用在装箱基本类型的类中的静态comparet方法,或者在Comparator接口中使用比较器构造方法。