5.2 java.lang.Object类的重要方法

前面说过,所有类都直接或间接扩展 java.lang.Object 类。这个类定义了很多有用的方法,而且你编写的类可以覆盖这些方法。示例 5-1 中的类覆盖了这些方法。这个示例之后的几节,说明各个方法的默认实现,以及为什么要覆盖。

这个示例使用了前一章介绍的多个类型系统的扩展功能。首先,这个示例使用参数化类型(或叫泛型)实现 Comparable 接口。其次,这个示例使用 @Override 注解,强调(并让编译器确认)某些方法覆盖了 Object 类中对应的方法。

示例 5-1:一个类,覆盖 Object 类的重要方法

  1. // 这个类表示圆形,位置和半径都不能改变
  2. public class Circle implements Comparable<Circle> {
  3. // 这些字段存储圆心的坐标和圆的半径
  4. // 使用private是为了封装数据,使用final是为了禁止修改
  5. private final int x, y, r;
  6. // 基本构造方法:使用指定的值初始化字段
  7. public Circle(int x, int y, int r) {
  8. if (r < 0) throw new IllegalArgumentException("negative radius");
  9. this.x = x; this.y = y; this.r = r;
  10. }
  11. // 这个“副本构造方法”创建一个副本,可代替clone()方法
  12. public Circle(Circle original) {
  13. x = original.x; // 直接从源对象中复制字段的值
  14. y = original.y;
  15. r = original.r;
  16. }
  17. // 公开的访问器方法,用于访问私有字段
  18. // 这也是为了封装数据
  19. public int getX() { return x; }
  20. public int getY() { return y; }
  21. public int getR() { return r; }
  22. // 返回对象的字符串表示形式
  23. @Override public String toString() {
  24. return String.format("center=(%d,%d); radius=%d", x, y, r);
  25. }
  26. // 测试与另一个对象是否相等
  27. @Override public boolean equals(Object o) {
  28. // 引用同一个对象?
  29. if (o == this) return true;
  30. // 类型不对,但不是null?
  31. if (!(o instanceof Circle)) return false;
  32. Circle that = (Circle) o; // 校正为Circle类型
  33. if (this.x == that.x && this.y == that.y && this.r == that.r)
  34. return true; // 如果所有字段的值都相等
  35. else
  36. return false; // 如果字段的值不相等
  37. }
  38. // 有哈希码的对象才能在哈希表中使用
  39. // 相等的对象必须具有相等的哈希码
  40. // 不相等的对象可以具有相等的哈希码,但要尽量避免出现这种情况
  41. // 因为我们覆盖了equals()方法,所以必须覆盖这个方法
  42. @Override public int hashCode() {
  43. int result = 17;// 这个哈希码算法出自Joshua Bloch写的Effective Java
  44. result = 37*result + x;
  45. result = 37*result + y;
  46. result = 37*result + r;
  47. return result;
  48. }
  49. // 这个方法由Comparable接口定义
  50. // 比较这个Circle对象和另一个Circle对象
  51. // 如果这个对象小于另一个对象,返回负数
  52. // 如果这个对象等于另一个对象,返回零
  53. // 如果这个对象大于另一个对象,返回正数
  54. // Circle对象按照从上到下、从左到右排序,然后再比较半径的大小
  55. public int compareTo(Circle that) {
  56. // y坐标较大的圆较小
  57. long result = (long)that.y - this.y;
  58. // 如果y坐标相同,再比较x坐标
  59. if (result==0) result = (long)this.x - that.x;
  60. // 如果x坐标相同,再比较半径
  61. if (result==0) result = (long)this.r - that.r;
  62. // 相减时必须使用long类型的值,因为较大的正数和较小的负数
  63. // 之差可能会溢出int类型的取值范围。但是不能返回long类型的值
  64. // 因此返回表示符号的int类型值
  65. return Long.signum(result);
  66. }
  67. }

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:只要左侧操作数是 nullinstanceof 运算符的计算结果就是 false

5.2.3 hashCode()方法

只要覆盖了 equals() 方法,就必须覆盖 hashCode() 方法。hashCode() 方法返回一个整数,用于哈希表数据结构。如果两个对象经 equals() 方法测试是相等的,它们就要具有相同的哈希码。不相等的对象要具有不相等的哈希码(为了哈希表的操作效率),这一点很重要,但不是强制要求,最低要求是不相等的对象不能共用一个哈希码。为了满足最低要求,hashCode() 方法要使用稍微复杂的算法或位操作。

Object.hashCode() 方法和 Object.equals() 方法协同工作,返回对象的哈希码。这个哈希码基于对象的身份生成,而不是对象的相等性。(如果需要使用基于身份的哈希码,可以通过静态方法 System.identityHashCode() 获取 Object.hashCode() 方法的返回值。)

5.2 java.lang.Object类的重要方法 - 图1 如果覆盖了 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 对象的副本:

  1. Circle original = new Circle(1, 2, 3); // 普通构造方法
  2. Circle copy = new Circle(original); // 副本构造方法

clone() 方法很难正确实现,而副本构造方法实现起来更容易也更安全。若想让 Circle 类可克隆,要在 implements 子句中加入 Cloneable,然后在类的主体中添加下述方法:

  1. @Override public Object clone() {
  2. try { return super.clone(); }
  3. catch(CloneNotSupportedException e) { throw new AssertionError(e); }
  4. }