尽管 Object 是一个具体类,但设计它主要是为了扩展。它所有的非 final 方法(equals、hashCode、toString、clone和finalize )都有明确的通用约定( general contract), 因为它们设计成是要被覆盖( override )的。
覆盖 equals 方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖 equals 方法,在这种情况下,类的每个实例都只与它自身相等。如果满足了以下任何一个条件,这就正是所期望的结果:
不严格地说,等价关系是一个操作符,将一组元素划分到其元素与另一个元素等价的分组中。这些分组被称作等价类(equivalence class)。从用户的角度来看,对于有用的 equals 方法,每个等价类中的所有元素都必须是可交换的。
在覆盖 equals 方法的时候,必须要遵守它的通用约定。下面是约定的内容,来自 Object 的规范。 equals 方法实现了等价关系(equivalence relation)其属性如下:
现在我们按照顺序逐一查看以下5个要求:
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
...
}
这项测试是没必要的。为了测试其参数的同等性,equals方法必须先把参数转换成适当的类型,以便可以调用它的访问方法,或者访问它的域。在进行转换之前,equals方法必须使用 instanceof 操作符,检查其参数的类型是否正确:
@Override
public boolean equals(Object obj) {
if (!(obj instanceof EqualsDemo)) {
return false;
}
EqualsDemo ed = (EqualsDemo) obj;
...
}
如果漏掉了这一步的类型检查,并且传递给 equals 方法的参数又是错误的类型,那么 equals 方法将会抛出 ClassCastException 异常,这就违反了 equals 约定。但是,如果 instanceof 的第一个操作数为 null, 那么,不管第二个操作数是哪种类型,instanceof 操作符都指定应该返回 false。因此,如果把 null 传给 equals 方法,类型检查就会返回 false,所以不需要显式的 null 检查。
域的比较顺序可能会影响 equals 方法的性能。为了获得最佳的性能,应该最先比较最有可能不一致的域,或者是开销最低的域,最理想的情况是两个条件同时满足的域。
在编写完 equals 方法之后,应该问自己三个问题:它是否对称的、传递的、一致的?并且不要只是自问,还要编写单元测试来检验这些特性,除非用 AutoValue 生成 equals 方法,在这种情况下就可以放心地省略测试。
下面是最后的一些告诫:
总而言之,不要轻易覆盖 equals 方法,除非迫不得已。因为在许多情况下,从 Object 处继承的实现正是你想要的。如果覆盖 equals,一定要比较这个类的所有关键域,并且查看它们是否遵守 equals 合约的所有五个条款。
在每个覆盖了 equals 方法的类中,都必须覆盖 hashCode 方法。如果不这样做的话,就会违反 hashCode 的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括 HashMap 和 HashSet。下面是约定的内容,摘自 Object 规范:
因没有覆盖 hashCode 而违反的关键约定是第二条:相等的对象必须具有相等的散列码(hash code)。
一个好的散列函数通常倾向于“为不相等的对象产生不相等的散列码”。这正是 hashCode 约定中的第三条的含义。理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的 int 值上。
1、声明一个 int 变量并命名为 result,将它初始化为对象中第一个关键域的散列码 c,如步骤2.1中计算所示(关键域是指影响 equals 比较的域)。 2、对象中剩下的每一个关键域 f 都完成以下步骤: 2.1 为该域计算 int 类型的散列码 c: 2.1.1 如果该域是基本类型,则计算 Type.hashCode(f),这里的 Type 是装箱基本类型的类,与 f 的类型相对于。 2.1.2 如果该域是一个对象引用,并且该类的 equals 方法通过递归地调用 equals 的方式来比较这个域,则同样为这个域递归地调用 hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用 hashCode。如果这个域的值为 null,则返回0(或者其他某个常数,但通常是0)。 2.2 按照下面的公式,把步骤2.1 中计算得到的散列码 c 合并到 result中:
result = 31 * result + c;
3、返回 result。
写好了 hashCode 方法之后,问问自己“相等的实例是否都具有相等的散列码”。
之所以选择31,是因为它是一个奇素数。 31有个很好的特性,即用移位和减法来代替乘法,可以得到更好的性能:
31 * i == (i << 5) - i
“延迟初始化”散列码,即一直到 hashCode 被第一次调用的时候才初始化。注意 hashCode 域的初始值(0)一般不能成为创建的实例的散列码:
private int hashCode; // 初始化0
@Test
public int hashCodeDemo() {
int result = hashCode;
if (result == 0) {
// 省略 result 计算
//result = 31 * result + Short.hashCode(str);
hashCode = result;
}
return result;
}
不要试图从散列码计算中排除掉一个对象的关键域来提高性能。
总而言之,每当覆盖 equals 方法时都必须覆盖 hashCode, 否则程序将无法正确运行。hashCode 方法必须遵守 Object 规定的通用约定,并且必须完成一定的工作,将不相等的散列码分配给不相等的实例。
toString 约定进一步指出,“建议所有的子类都覆盖这个方法”。这是一个很好的建议,真的!
提供好的 toString 实现可以使类用起来更加舒适,使用了这个类的系统也更易于调试。
在实际应用中,toString 方法应该返回对象中包含额所有值得关注的信息
总而言之,要在你编写的每一个可实例化的类中覆盖 Object 的 toString 实现,除非已经在超类中这么做了。这样会使类使用起来更加舒适,也更易于调试。toString 方法应该以美观的格式返回一个关于对象的简洁、有用的描述。
Cloneable 接口的目的是作为对象的一个 mixin接口(mixin interface),表明这样的对象允许克隆(clone)。遗憾的是,它并没有成功地达到这个目的。它的主要缺陷在于缺少一个 clone 方法,而 Object 的 clone 方法是受保护的。如果不借助于反射(reflection),就不能仅仅因为一个对象实现了 Cloneable,就调用 clone 方法。即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的 clone 方法。
既然Cloneab1e接口并没有包含任何方法,那么它到底有什么作用呢?它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。这是接口的一种极端非典型的用法,也不值得仿效。通常情况下,实现接口是为了表明类可以为它的客户做些什么。然而,对于Cloneab1e接口,它改变了超类中受保护的方法的行为。
虽然规范中没有明确指出,事实上,实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法。为了达到这个目的,类及其所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。由此得到一种语言之外的(extralinguistic)机制:它无须调用构造器就可以创建对象。
clone方法的通用约定是非常弱的,下面是来自 Object 规范中的约定内容: 创建和返回该对象的一个拷贝。这个“拷贝”的精准含义取决于该对象的类。一般的含义是,对于任何对象 x,表达式
x.clone() != x
将会返回结果 true,并且表达式
x.clone().getClass() == x.getClass()
将会返回结果 true,但这些都不是绝对的要求。虽然通常情况下,表达式
x.clone().equals(x)
将会返回结果 true,但是,这也不是一个绝对的要求。 按照约定,这个方法返回的对象应该通过调用 super.clone 获得。如果类及其超类(Object除外)遵守这一约定,那么:
x.clone().getClass() == x.getClass()
按照约定,返回的对象应该不依赖于被克隆的对象。为了成功地实现这种独立性,可能需要在 super.clone 返回对象之前,修改对象的一个或更多个域。