内容


第 13 单元:对象的后续处理

增加类和方法的灵活性

Comments

开始之前

本单元是 “Java 编程入门” 学习路径的一部分。尽管各单元中讨论的概念具有独立性,但实践组件是在您学习各单元的过程中逐步建立起来的,推荐您在继续学习之前复习 前提条件、设置和单元细节

单元目标

  • 了解方法重载和重写
  • 能够比较一个对象与另一个对象
  • 了解如何和何时使用类变量和方法

重载方法

是时候了解一下 Person 类了。Person 现在比较有用,但没有达到应有的实用程度。我们首先通过重载Person 的方法来增强它。

创建两个具有相同名称和不同参数列表(即不同的参数数量或类型)的方法时,您就拥有了一个重载 方法。在运行时,JRE 基于传递给它的参数来决定调用您的重载方法的哪个变体。

假设 Person 需要两个方法来打印其当前状态的审计结果。我将这两个方法都命名为 printAudit()。将清单 1 中的重载方法粘贴到 Eclipse 编辑器视图中的 Person 类中:

清单 1. printAudit():一个重载方法
public void printAudit(StringBuilder buffer) {
   buffer.append("Name=");
   buffer.append(getName());
   buffer.append(",");
   buffer.append("Age=");
   buffer.append(getAge());
   buffer.append(",");
   buffer.append("Height=");
   buffer.append(getHeight());
   buffer.append(",");
   buffer.append("Weight=");
   buffer.append(getWeight());
   buffer.append(",");
   buffer.append("EyeColor=");
   buffer.append(getEyeColor());
   buffer.append(",");
   buffer.append("Gender=");
   buffer.append(getGender());
}

public void printAudit(Logger l) {
   StringBuilder sb = new StringBuilder();
   printAudit(sb);
   l.info(sb.toString());
}

您拥有 printAudit() 的两个重载版本,一个版本甚至使用了另一个版本。通过提供两个版本,调用方能够选择如何打印类的审计结果。Java 运行时依据传递的参数而调用正确的方法。

在使用重载方法时,请记住两条重要规则

  • 不能仅通过更改一个方法的返回类型来重载它。
  • 不能拥有两个具有相同名称和相同参数列表的方法。

如果违背这些规则,编译器就会抛出错误。

重写方法

如果一个子类提供其父类中定义的方法的自有实现,这被称为方法重写。要了解方法重写有何用处,需要在您的 Employee 类上执行一些工作。请观看下面的视频,了解如何设置 Employee 类和在该类中执行方法重写。观看视频之后,我将更详细地分析该代码,同时简要复述一下这些步骤。

该视频还演示了如何重写 equals() 方法和自动生成 equals()hashCode() 类,我将在本单元的 “比较对象” 部分详细介绍这些操作。

Java 编程入门,第 13 单元:对象的后续处理

Java 编程入门,第 13 单元:对象的后续处理
Java 编程入门,第 13 单元:对象的后续处理

点击查看视频演示查看抄本

Employee:Person 的一个子类

回想一下 第 3 单元:面向对象的概念和原理Employee 类可以是 Person 的子类(或孩子),它包含更多属性,比如纳税人识别号、员工编号、招入日期和工资。您现在将声明 Employee 类,稍后将添加这些属性。

要声明 Employee 类,可在 Eclipse 中右键单击 com.makotojava.intro 包。单击 New > Class...,在 New Java Class 对话框中,输入 Employee 作为类名,Person 作为它的超类。单击 Finish,可以在一个编辑窗口中看到 Employee 类代码。

您不是明确需要声明构造方法,但无论如何,让我们实现两个构造方法。让 Employee 类编辑窗口拥有焦点,转到 Source > Generate Constructors from Superclass...。在 Generate Constructors from Superclass 对话框中,选择两个构造方法并单击 OK。您现在拥有一个与清单 2 类似的 Employee 类。

清单 2. Employee
package com.makotojava.intro;

public class Employee extends Person {

  public Employee() {
    super();
    // TODO Auto-generated constructor stub
  }
  
  public Employee(String name, int age, int height, int weight,
  String eyeColor, String gender) {
    super(name, age, height, weight, eyeColor, gender);
    // TODO Auto-generated constructor stub
  }

}

Employee 是 Person 的子类

Employee 继承它的父类 Person 的属性和行为。添加 Employee 自己的一些属性,如清单 3 中第 7 到第 9 行所示。

清单 3. 包含 Person 的属性的 Employee
package com.makotojava.intro;

import java.math.BigDecimal;

public class Employee extends Person {

  private String taxpayerIdentificationNumber;
  private String employeeNumber;
  private BigDecimal salary;

  public Employee() {
    super();
  }
  public String getTaxpayerIdentificationNumber() {
    return taxpayerIdentificationNumber;
  }
  public void setTaxpayerIdentificationNumber(String taxpayerIdentificationNumber) {
    this.taxpayerIdentificationNumber = taxpayerIdentificationNumber;
  }

  // Other getter/setters...
}

不要忘了生成新属性的 getter 和 setter,就像在 第 5 单元:您的第一个 Java 类 中对 Person 所做的那样。

重写 printAudit() 方法

现在您将重写 printAudit() 方法(参见 清单 1),该方法用于格式化一个 Person 实例的当前状态。Employee 继承了 Person 的行为。如果实例化 Employee,设置它的属性,然后调用 printAudit() 的一个重载方法,调用将成功完成。但是,生成的审计结果没有完整表示一个 EmployeeprintAudit() 无法格式化特定于某个 Employee 的属性,因为 Person 不知道它们。

解决方案是重写 printAudit() 的一个接受 StringBuilder 作为参数的重载方法,添加代码来打印特定于 Employee 的属性。

在编辑器窗口中打开或在 Project Explorer 视图中选定 Employee,转到 Source > Override/Implement Methods...。在 Override/Implement Methods 对话框中,选择 printAudit()StringBuilder 重载方法并单击 OK。Eclipse 为您生成了方法存根,然后您可填入剩余部分,类似这样:

@Override
public void printAudit(StringBuilder buffer) {
  // Call the superclass version of this method first to get its attribute values
  super.printAudit(buffer);

  // Now format this instance's values
  buffer.append("TaxpayerIdentificationNumber=");
  buffer.append(getTaxpayerIdentificationNumber());
  buffer.append(","); buffer.append("EmployeeNumber=");
  buffer.append(getEmployeeNumber());
  buffer.append(","); buffer.append("Salary=");
  buffer.append(getSalary().setScale(2).toPlainString());
}

请注意对 super.printAudit() 的调用。您在这里所做的是要求 (Person) 超类向 printAudit() 显示其行为,然后使用 Employee 类型的 printAudit() 行为来扩充它。

不需要先调用 super.printAudit(),只不过,先打印这些属性似乎是一个不错的主意。事实上,您完全不需要调用 super.printAudit()。如果不调用它,则必须在 Employee.printAudit() 方法中自行格式化来自 Person 的属性,否则它们不会包含在审计输出中。

比较对象

Java 语言提供了两种比较对象的方法:

  • == 运算符
  • equals() 方法

使用 == 比较对象

== 语法比较对象是否相等,只有在 ab 拥有相同的值时,a == b 才返回 true。对于对象,需要两个对象引用同一个对象实例。对于原语,需要它们的值相等

假设您为 Employee 生成一个 JUnit 测试(您已在第 5 单元:您的第一个 Java 类 中了解了如何做)。清单 4 中显示了 JUnit 测试。

清单 4. 使用 == 比较对象
public class EmployeeTest {
  @Test
  public void test() {
    int int1 = 1;
    int int2 = 1;
    Logger l = Logger.getLogger(EmployeeTest.class.getName());
    
    l.info("Q: int1 == int2?           A: " + (int1 == int2));
    Integer integer1 = Integer.valueOf(int1);
    Integer integer2 = Integer.valueOf(int2);
    l.info("Q: Integer1 == Integer2?   A: " + (integer1 == integer2));
    integer1 = new Integer(int1);
    integer2 = new Integer(int2);
    l.info("Q: Integer1 == Integer2?   A: " + (integer1 == integer2));
    Employee employee1 = new Employee();
    Employee employee2 = new Employee();
    l.info("Q: Employee1 == Employee2? A: " + (employee1 == employee2));
  }
}

在 Eclipse 中运行清单 4 的代码(在 Project Explorer 视图中选择 Employee,然后选择 Run As > JUnit Test),以生成以下输出:

Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: int1 == int2?           A: true
Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: Integer1 == Integer2?   A: true
Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: Integer1 == Integer2?   A: false
Sep 18, 2015 5:09:56 PM com.makotojava.intro.EmployeeTest test
INFO: Q: Employee1 == Employee2? A: false

清单 4 中的第一种情况下,原语的值相同,所以 == 运算符返回 true。在第二种情况下,Integer 对象引用同一个实例,所以 == 同样返回 true。在第三种情况下,尽管 Integer 对象包含相同的值,但 == 返回 false,因为 integer1integer2 引用了不同的对象。可将 == 视为对 “相同的对象实例” 的测试。

使用 equals() 比较对象

equals() 是每种 Java 语言对象都可以自由使用的方法,因为它被定义为 java.lang.Object 的一个实例方法(每个 Java 对象都继承该对象)。

可以像这样调用 equals()

a.equals(b);

此语句调用对象 aequals() 方法,向它传递对象 b 的引用。默认情况下,Java 程序使用 == 语法检查两个对象是否相同。但是因为 equals() 是一种方法,所以它可以被重写。将 清单 4 中的 JUnit 测试案例与清单 5 中的测试案例(我称之为 anotherTest())进行比较,后者使用了 equals() 来比较两个对象:

清单 5. 使用 equals() 比较对象
@Test
public void anotherTest() {
  Logger l = Logger.getLogger(Employee.class.getName());
  Integer integer1 = Integer.valueOf(1);
  Integer integer2 = Integer.valueOf(1);
  l.info("Q: integer1 == integer2 ? A: " + (integer1 == integer2));
  l.info("Q: integer1.equals(integer2) ? A: " + integer1.equals(integer2));
  integer1 = new Integer(integer1);
  integer2 = new Integer(integer2);
  l.info("Q: integer1 == integer2 ? A: " + (integer1 == integer2));
  l.info("Q: integer1.equals(integer2) ? A: " + integer1.equals(integer2));
  Employee employee1 = new Employee();
  Employee employee2 = new Employee();
  l.info("Q: employee1 == employee2 ? A: " + (employee1 == employee2));
  l.info("Q: employee1.equals(employee2) ? A : " + employee1.equals(employee2));
}

运行清单 5 的代码会生成以下输出:

Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1 == integer2 ? A: true
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1.equals(integer2) ? A: true
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1 == integer2 ? A: false
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: integer1.equals(integer2) ? A: true
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: employee1 == employee2 ? A: false
Sep 19, 2015 10:11:57 AM com.makotojava.intro.EmployeeTest anotherTest
INFO: Q: employee1.equals(employee2) ? A : false

一条关于比较整数的说明

清单 5 中,如果 == 返回 trueIntegerequals() 方法就会返回 true,不必对此感到奇怪。但请注意第二种情况中发生的事情,您创建了都包含值 1 的不同对象:== 返回 false,因为 integer1integer2 引用了不同的对象;但 equals() 返回 true

JDK 的编写者认为,对于 Integerequals() 的含义与默认含义不同(回想一下,默认含义是比较对象引用,看看它们是否引用同一个对象)。对于 Integer,在底层(装箱)的 int 值相同时,equals() 返回 true

对于 Employee,您没有重写 equals(),所以(使用 == 的)默认行为返回了您期望的结果,因为 employee1employee2 引用了不同的对象。

然后,对于您编写的任何对象,您可定义适合您编写的应用程序的 equals() 的含义。

重写 equals()

可通过重写 Object.equals() 的默认行为,定义 equals() 对您的应用程序的对象的含义— 您可以在 Eclipse 中这么做。让 Employee 在 IDE 的源代码窗口中拥有焦点,选择 Source > Override/Implement Methods。您希望实现 Object.equals() 超类方法。所以应在方法列表中找到要重写或实现的 Object,选择 equals(Object) 方法,然后单击 OK。Eclipse 生成正确的代码并将它放在您的源文件中。

可以合理地理解为,如果两个 Employee 对象的状态相等,则这两个对象相等。也就是说,如果它们的值(姓名和年龄)相同,则它们相等。

自动生成 equals()

Eclipse 可根据您为某个类定义的实例变量(属性)来生成一个 equals() 方法。因为 EmployeePerson 的子类,所以您首先为 Person 生成 equals()。在 Eclipse Project Explorer 视图中,右键单击 Person 并选择 Generate hashCode() and equals()。在打开的对话框中,单击 Select All 以包含 hashCode()equals() 方法中的所有属性,然后单击 OK。Eclipse 生成一个类似于清单 6 的 equals() 方法。

清单 6. Eclipse 生成的一个 equals() 方法
@Override
public boolean equals(Object obj) {
  if (this == obj)
    return true;
  if (obj == null)
    return false;
  if (getClass() != obj.getClass())
    return false;
  Person other = (Person) obj;
  if (age != other.age)
    return false;
  if (eyeColor == null) {
    if (other.eyeColor != null)
      return false;
  } else if (!eyeColor.equals(other.eyeColor))
    return false;
  if (gender == null) {
    if (other.gender != null)
      return false;
  } else if (!gender.equals(other.gender))
    return false;
  if (height != other.height)
    return false;
  if (name == null) {
    if (other.name != null)
      return false;
  } else if (!name.equals(other.name))
    return false;
  if (weight != other.weight)
    return false;
  return true;
}

Eclipse 生成的 equals() 方法看起来很复杂,但它的作用很简单:如果传入的对象与清单 6 中的对象相同,那么 equals() 会返回 true。如果传入的对象为 null(表示缺少),那么它会返回 false

接下来,该方法检查 Class 对象是否相同(表示传入的对象必须是一个 Person 对象)。如果相同,则检查传入的对象的每个属性值,查看它们是否与给定的 Person 实例的状态逐值匹配。如果属性值为 null,equals() 会检查尽可能多的次数,如果这些值匹配,则会认为这些对象相等。您可能不希望每个程序都具有此行为,但它适合大部分用途。

练习

现在,在 Eclipse 中执行两个引导式练习,以便进一步完善 PersonEmployee

练习 1:为 Employee 生成一个 equals()

尝试执行 “自动生成 equals()” 中的步骤,为 Employee 生成一个 equals()。生成 equals() 后,向它添加下面这个 JUnit 测试案例(我称之为 yetAnotherTest()):

@Test
public void yetAnotherTest() {
  Logger l = Logger.getLogger(Employee.class.getName());
  Employee employee1 = new Employee();
  employee1.setName("J Smith");
  Employee employee2 = new Employee();
  employee2.setName("J Smith");
  l.info("Q: employee1 == employee2?      A: " + (employee1 == employee2));
  l.info("Q: employee1.equals(employee2)? A: " + employee1.equals(employee2));    
}

如果运行该代码,您会看到以下输出:

Sep 19, 2015 11:27:23 AM com.makotojava.intro.EmployeeTest yetAnotherTest
INFO: Q: employee1 == employee2?      A: false
Sep 19, 2015 11:27:23 AM com.makotojava.intro.EmployeeTest yetAnotherTest
INFO: Q: employee1.equals(employee2)? A: true

在本例中,单单 Name 上的一个匹配值就足以让 equals() 相信两个对象相等。尝试向此示例添加更多属性,看看您会获得什么。

练习 2:重写 toString()

还记得本单元开头的 printAudit() 方法吗?如果您认为它的工作太困难了,那就对了。将对象的状态格式化为 String 是一种常见模式,以至于 Java 语言的设计者(意料之中地)已在一个名为 toString() 的方法中将它内置到 Object 自身中。toString() 的默认实现不是特别有用,但每个对象都有一个。在本练习中,可以重写 toString() 来使它更有用。

如果您猜测 Eclipse 可为您生成一个 toString() 方法,那么您猜对了。返回到 Project Explorer 中并右键单击 Person 类,然后选择 Source > Generate toString()...。在对话框中,选择所有属性并单击 OK。现在对 Employee 执行相同的操作。Eclipse 为 Employee 生成的代码如清单 7 所示。

清单 7. Eclipse 生成的一个 toString() 方法
@Override
public String toString() {
  return "Employee [taxpayerIdentificationNumber=" + taxpayerIdentificationNumber + ", 
      employeeNumber=" + employeeNumber + ", salary=" + salary + "]";
}

Eclipse 为 toString 生成的代码不包含超类的 toString()Employee 的超类为 Person)。使用 Eclipse,您可以通过这个重写方法快速处理这种情况:

@Override
public String toString() {
  return super.toString() + "Employee [taxpayerIdentificationNumber=" + taxpayerIdentificationNumber + 
    ", employeeNumber=" + employeeNumber + ", salary=" + salary + "]";
}

添加的 toString() 使 printAudit() 得到大大简化:

@Override
  public void printAudit(StringBuilder buffer) {
  buffer.append(toString());
}

toString() 现在执行了格式化对象当前状态的主要工作,您只需将它返回的值放入 StringBuilder 中并返回。

如果只是为了提供支持,我推荐始终在您的类中实现 toString()。几乎不可避免的是,在某个时刻,您希望在您的应用程序运行时查看一个对象的状态是什么,toString() 是一个实现此目的的不错挂钩。

类成员

每个对象实例都拥有变量和方法,而且对于每个实例,准确的行为是不同的,因为它基于对象实例的状态。您在 PersonEmployee 上拥有的变量和方法是实例 变量和方法。要使用它们,要么必须实例化需要的类,要么必须拥有该实例的引用。

类也可以拥有 变量和方法(被称为类成员)。类变量使用 static 关键字声明。类变量与实例变量之间的区别在于:

  • 类的每个实例共享一个类变量的单一副本。
  • 可以在类本身上调用类方法,而无需拥有实例。
  • 类方法只能访问类变量。
  • 实例方法可访问类变量,但类方法无法访问实例变量。

何时添加类变量和方法才合理?最佳的经验规则是很少添加,以便您不会重用它们。尽管如此,将类变量和方法用于以下用途是个不错的主意:

  • 声明类的任何实例可使用的常量(而且它们的值在开发时是固定的)
  • 用在具有实用程序方法的类上,而且这些方法从不需要该类的实例(比如 Logger.getLogger()

类变量

要创建类变量,可在声明它时使用 static 关键字:

accessSpecifier static variableName [= initialValue];

备注:这里的方括号表示它们的内容是可选的。方括号不是声明语法的一部分。

JRE 创建内存空间来存储一个类的每个实例的每个实例 变量。相反,JRE 仅创建每个 变量的单个副本,无论有多少个实例。它在首次加载类时(也即它在程序中首次遇到该类时)执行此操作。类的所有实例共享该变量的这个副本。这使类变量成为了所有实例都应能使用的常量的不错选择。

例如,您已将 PersonGender 属性声明为 String,但没有对它设置任何约束。清单 8 显示了类变量的一种常见用途。

清单 8. 使用类变量
public class Person {
  //. . .
  public static final String GENDER_MALE = "MALE";
  public static final String GENDER_FEMALE = "FEMALE";

  // . . .
  public static void main(String[] args) {
  Person p = new Person("Joe Q Author", 42, 173, 82, "Brown", GENDER_MALE);
    // . . .
  }
  //. . .
}

声明常量

通常,常量:

  • 用全大写形式命名
  • 使用多个用下划线分隔的单词命名
  • 声明为 final(以便它们的值无法修改)
  • 使用 public 访问修饰符声明(以便其他需要按名称引用它们的值的类能够访问它们)

清单 8 中,要在 Person 构造方法调用中使用 MALE 常量,您可以引用它的名称。要在类外使用一个常量,您可以将声明它的类的名称放在它前面:

String genderValue = Person.GENDER_MALE;

类方法

您已调用静态 Logger.getLogger() 方法多次 — 只要您检索一个 Logger 实例来将输出写入到控制台时就会调用它。但是请注意,不需要 Logger 实例也可以这么做。但是您引用了 Logger 类,这是执行类方法 调用的语法。与类变量一样,static 关键字将 Logger(在本例中)标识为类方法。出于这个原因,类方法有时也称为静态方法

现在,可以结合您学到的静态变量和方法的知识,在 Employee 上创建一个静态方法。声明一个 private static final 变量来持有一个 Logger,所有实例都共享该对象,而且可通过在 Employee 类上调用 getLogger() 来访问它。清单 9 展示了如何做。

清单 9. 创建一个类(或静态)方法
public class Employee extends Person {
  private static final Logger logger = Logger.getLogger(Employee.class.getName());
  //. . .
  public static Logger getLogger() {
    return logger;
  }

}

清单 9 中发生了两件重要的事:

  • Logger 实例声明时使用了 private 访问级别,所以 Employee 外的任何类都无法直接访问该引用。
  • Logger 在加载类时初始化 — 因为您使用了 Java 初始化器语法来向它提供值。

要检索 Employee 类的 Logger 对象,可执行以下调用:

Logger employeeLogger = Employee.getLogger();

进一步探索

Java 教程:重写和隐藏方法

Java - 重写

Java 教程:定义方法

Java 中的 ==equals() 有何区别?

Java - toString() 方法

如何在 Java 中使用 toString() 方法?

上一单元:编写良好的 Java 代码下一单元:异常


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=1038944
ArticleTitle=第 13 单元:对象的后续处理
publish-date=10252016