5.2 java.lang.Object类的重要方法
前面说过,所有类都直接或间接扩展 java.lang.Object 类。这个类定义了很多有用的方法,而且你编写的类可以覆盖这些方法。示例 5-1 中的类覆盖了这些方法。这个示例之后的几节,说明各个方法的默认实现,以及为什么要覆盖。
这个示例使用了前一章介绍的多个类型系统的扩展功能。首先,这个示例使用参数化类型(或叫泛型)实现 Comparable 接口。其次,这个示例使用 @Override 注解,强调(并让编译器确认)某些方法覆盖了 Object 类中对应的方法。
示例 5-1:一个类,覆盖 Object 类的重要方法
// 这个类表示圆形,位置和半径都不能改变public class Circle implements Comparable<Circle> {// 这些字段存储圆心的坐标和圆的半径// 使用private是为了封装数据,使用final是为了禁止修改private final int x, y, r;// 基本构造方法:使用指定的值初始化字段public Circle(int x, int y, int r) {if (r < 0) throw new IllegalArgumentException("negative radius");this.x = x; this.y = y; this.r = r;}// 这个“副本构造方法”创建一个副本,可代替clone()方法public Circle(Circle original) {x = original.x; // 直接从源对象中复制字段的值y = original.y;r = original.r;}// 公开的访问器方法,用于访问私有字段// 这也是为了封装数据public int getX() { return x; }public int getY() { return y; }public int getR() { return r; }// 返回对象的字符串表示形式@Override public String toString() {return String.format("center=(%d,%d); radius=%d", x, y, r);}// 测试与另一个对象是否相等@Override public boolean equals(Object o) {// 引用同一个对象?if (o == this) return true;// 类型不对,但不是null?if (!(o instanceof Circle)) return false;Circle that = (Circle) o; // 校正为Circle类型if (this.x == that.x && this.y == that.y && this.r == that.r)return true; // 如果所有字段的值都相等elsereturn false; // 如果字段的值不相等}// 有哈希码的对象才能在哈希表中使用// 相等的对象必须具有相等的哈希码// 不相等的对象可以具有相等的哈希码,但要尽量避免出现这种情况// 因为我们覆盖了equals()方法,所以必须覆盖这个方法@Override public int hashCode() {int result = 17;// 这个哈希码算法出自Joshua Bloch写的Effective Javaresult = 37*result + x;result = 37*result + y;result = 37*result + r;return result;}// 这个方法由Comparable接口定义// 比较这个Circle对象和另一个Circle对象// 如果这个对象小于另一个对象,返回负数// 如果这个对象等于另一个对象,返回零// 如果这个对象大于另一个对象,返回正数// Circle对象按照从上到下、从左到右排序,然后再比较半径的大小public int compareTo(Circle that) {// y坐标较大的圆较小long result = (long)that.y - this.y;// 如果y坐标相同,再比较x坐标if (result==0) result = (long)this.x - that.x;// 如果x坐标相同,再比较半径if (result==0) result = (long)this.r - that.r;// 相减时必须使用long类型的值,因为较大的正数和较小的负数// 之差可能会溢出int类型的取值范围。但是不能返回long类型的值// 因此返回表示符号的int类型值return Long.signum(result);}}
5.2.1 toString()方法
toString() 方法的作用是返回对象的文本表示形式。连接字符串或使用 System.out.println() 等方法时,会自动在对象上调用这个方法。给对象提供文本表示形式,十分利于调试或记录日志,而且精心编写的 toString() 方法还能给报告生成等任务提供帮助。
Object 类中的 toString() 方法返回的字符串由对象所属的类名和对象的十六进制形式哈希码(由 hashCode() 方法计算得到,本章稍后会介绍)组成。这个默认的实现方式提供了对象的类型和标识两个基本信息,但一般并没什么用。示例 5-1 定义的 toString() 方法,返回一个人类可读的字符串,包含 Circle 类每个字段的值。
5.2.2 equals()方法
== 运算符测试两个引用是否指向同一个对象。如果要测试两个不同的对象是否相等,必须使用 equals() 方法。任何类都能覆盖 equals() 方法,定义专用的相等比较方式。Object.equals() 方法直接使用 == 运算符,只有两个对象是同一个对象时,才判定二者相等。
仅当两个不同的 Circle 对象的全部字段都相等时,示例 5-1 中定义的 equals() 方法才判定二者相等。注意,这个 equals() 方法先使用 == 运算符测试对象是否相同(一项优化措施),然后使用 instanceof 运算符检查另一个对象的类型,因为 Circle 对象只能和另一个 Circle 对象相等,而且 equals() 方法不能抛出 ClassCastException 异常。注意,instanceof 运算符还能排除 null:只要左侧操作数是 null,instanceof 运算符的计算结果就是 false。
5.2.3 hashCode()方法
只要覆盖了 equals() 方法,就必须覆盖 hashCode() 方法。hashCode() 方法返回一个整数,用于哈希表数据结构。如果两个对象经 equals() 方法测试是相等的,它们就要具有相同的哈希码。不相等的对象要具有不相等的哈希码(为了哈希表的操作效率),这一点很重要,但不是强制要求,最低要求是不相等的对象不能共用一个哈希码。为了满足最低要求,hashCode() 方法要使用稍微复杂的算法或位操作。
Object.hashCode() 方法和 Object.equals() 方法协同工作,返回对象的哈希码。这个哈希码基于对象的身份生成,而不是对象的相等性。(如果需要使用基于身份的哈希码,可以通过静态方法 System.identityHashCode() 获取 Object.hashCode() 方法的返回值。)
如果覆盖了
equals()方法,必须覆盖hashCode()方法,这样才能保证相等的对象具有相同的哈希码。如果不这么做,程序可能会出现难以排查的问题。
在示例 5-1 中,因为 equals() 方法根据三个字段的值判断对象是否相等,所以 hashCode() 方法也基于这三个字段计算对象的哈希码。从 hashCode() 方法的代码中可以明确看出,如果两个 Circle 对象的字段值都相等,那么它们的哈希码也相等。
注意,示例 5-1 中的 hashCode() 方法没有直接相加三个字段的值,返回总和。这种实现方式算是合理,但还不够,因为如果两个圆的半径一样,但 x 和 y 坐标对调,哈希码依然相同。多次相乘和相加后,哈希码的值域会变大,因此能显著降低两个不相等的 Circle 对象具有相同哈希码的可能性。Joshua Bloch 写的 Effective Java(Addison Wesley 出版)一书对如何合理编写 hashCode() 方法提供了一个有用的攻略,和示例 5-1 中使用的类似。
5.2.4 Comparable::compareTo()方法
示例 5-1 包含一个 compareTo() 方法。这个方法由 java.lang.Comparable 接口而不是 Object 定义。但是这个方法经常要实现,所以也放在这一节介绍。Comparable 接口和其中的 compareTo() 方法用于比较类的实例,方式类似于比较数字的 <、<=、> 和 >= 运算符。如果一个类实现了 Comparable 接口,就可以比较一个实例是小于、大于还是等于另一个实例。这也表明,实现 Comparable 接口的类可以排序。
因为 compareTo() 方法不在 Object 类中声明,所以由每个类自行决定实例能否排序。如果能排序就定义 compareTo() 方法,实现实例排序的方式。示例 5-1 定义的排序方式把 Circle 对象当成一个页面中的单词,然后再比较。首先从上到下排序圆:y 坐标大的圆小于 y 坐标小的圆。如果两个圆的 y 坐标相同,再从左到右排序:x 坐标小的圆小于 x 坐标大的圆。如果两个圆的 x 坐标和 y 坐标都相同,那就比较半径:半径小则圆也小。注意,按照这种排序方式,只有圆的三个字段都相等圆才相等。因此,compareTo() 方法定义的排序方式和 equals() 方法定义的相等条件是一致的。这么做非常合乎情理(但不是强制要求)。
compareTo() 方法返回一个 int 类型的值,这个值需要进一步说明。如果当前对象(this)小于传入的对象,compareTo() 方法应该返回一个负数;如果两个对象相等,应该返回 0;如果当前对象大于传入的对象,应该返回一个正数。
5.2.5 clone()方法
Object 类定义了一个名为 clone() 的方法,这个方法的作用是返回一个对象,并把这个对象的字段设为和当前对象一样。clone() 方法不常用,原因有两个。其一,只有类实现了 java.lang.Cloneable 接口,这个方法才有用。Cloneable 接口没有定义任何方法(是个标记接口),因此若想实现这个接口,只需在类签名的 implements 子句中列出这个接口即可。其二,clone() 方法声明为 protected,因此,如果想让其他类复制你的对象,你的类必须实现 Cloneable 接口,并覆盖 clone() 方法,而且要把 clone() 方法声明为 public。
示例 5-1 中的 Circle 类没有实现 Cloneable 接口,而是定义一个副本构造方法,用于创建 Circle 对象的副本:
Circle original = new Circle(1, 2, 3); // 普通构造方法Circle copy = new Circle(original); // 副本构造方法
clone() 方法很难正确实现,而副本构造方法实现起来更容易也更安全。若想让 Circle 类可克隆,要在 implements 子句中加入 Cloneable,然后在类的主体中添加下述方法:
@Override public Object clone() {try { return super.clone(); }catch(CloneNotSupportedException e) { throw new AssertionError(e); }}
如果覆盖了 