集册 Java 精简入门教程 对象深入使用

对象深入使用

欢马劈雪     最近更新时间:2020-08-25 03:31:53

492

Java 语言基础介绍了 Person 类,这个类比较有用,但尚未达到它应有的实用程度。在这里,您将开始学习用各种技术增强一个类(比如 Person),首先学习以下技术:

  • 重载方法
  • 覆盖方法
  • 将一个对象与另一个对象进行比较
  • 让代码更易于调试

您将开始通过重载某个方法来增强Person

重载方法

创建两个具有相同名称,但具有不同参数列表(即不同数量或类型的参数)的方法时,就拥有了一个 重载 方法。在运行时,Java 运行时环境(JRE;也称为 Java 运行时)根据传递给它的参数来决定调用哪个重载方法的变体。

假设 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() 类,我将在本单元的比较对象部分详细介绍这些操作。

Employee:Person 的一个子类

回想在前面教程中您学习过基本的 OOP 编程原理,Employee 可以是 Person 的一个子类(或 孩子 ),但拥有额外的属性,如纳税人识别编号、员工编号、招聘日期和工资。

要在一个名为 Employee.java 的文件中声明这样一个类,可在 Eclipse 中右键单击 com.nowjava.intro 包。单击 New > Class… 打开 New Java Class 对话框,如图 1 所示。

图 1. New Java Class 对话框

Project Explorer 中 New Java Class 对话框

输入 Employee 作为该类的名称, Person 作为它的超类,然后单击 Finish 。可在编辑窗口中看到 Employee 类。您没有明确要求声明一个构造函数,但仍然实现了两个构造函数。首先,确保 Employee 类编辑窗口是当前窗口,然后转到 Source > Generate Constructors from Superclass…。 此时会看到一个对话框,可在其中选择要实现的构造函数,如图 2 所示。选择两个构造函数并单击 OK

图 2. Generate Constructors from Superclass 对话框

创建构造函数的项目路径

Eclipse 会为您生成这些构造函数。现在拥有一个与清单 2 类似的 Employee 类。

清单 2. 改进后的新 Employee
package com.nowjava.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

从清单 3 中的第 7 行至第 9 行可以看到, Employee 继承了它的父类 Person 的属性和行为,同时也拥有自己的一些属性和行为。

清单 3. 包含 Person 属性的 Employee
package com.nowjava.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。第 1 部分 中介绍了如何完成该操作。

方法覆盖:printAudit()

现在正如我所承诺的,可以练习覆盖方法了。 您要覆盖 printAudit() 方法(参见清单 1 ),该方法可格式化 Person 实例的当前状态。 Employee 继承了 Person 的行为。如果实例化 Employee,设置它的属性,然后调用 printAudit() 的一个重载方法,则调用会成功完成。但是,生成的审计结果不会全面代表一个 Employee 。问题在于,printAudit() 无法格式化特定于 Employee 的属性,因为 Person 不知道这些属性。

解决方案是覆盖可将 StringBuilder 作为参数的 printAudit() 的重载方法,并添加代码来打印特定于 Employee 的属性。

要在 Eclipse IDE 中实现此解决方案,确保已在编辑器窗口中打开或已在 Project Explorer 视图中选择了 Employee。然后转到 Source > Override/Implement Methods…,此时会看到一个对话框(如图 3 所示),选择 printAudit()StringBuilder 重载方法,如图 3 所示,然后单击 OK

图 3. Override/Implement Methods 对话框

Override/Implement Methods 对话框

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 测试(您已在 Java 语言基础中的 “您的第一个 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.nowjava.intro.EmployeeTest test
INFO:Q: int1 == int2?           A: true
Sep 18, 2015 5:09:56 PM com.nowjava.intro.EmployeeTest test
INFO:Q:Integer1 == Integer2?   A: true
Sep 18, 2015 5:09:56 PM com.nowjava.intro.EmployeeTest test
INFO:Q:Integer1 == Integer2?   A: false
Sep 18, 2015 5:09:56 PM com.nowjava.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.nowjava.intro.EmployeeTest anotherTest
INFO:Q: integer1 == integer2 ?A: true
Sep 19, 2015 10:11:57 AM com.nowjava.intro.EmployeeTest anotherTest
INFO:Q: integer1.equals(integer2) ?A: true
Sep 19, 2015 10:11:57 AM com.nowjava.intro.EmployeeTest anotherTest
INFO:Q: integer1 == integer2 ?A: false
Sep 19, 2015 10:11:57 AM com.nowjava.intro.EmployeeTest anotherTest
INFO:Q: integer1.equals(integer2) ?A: true
Sep 19, 2015 10:11:57 AM com.nowjava.intro.EmployeeTest anotherTest
INFO:Q: employee1 == employee2 ?A: false
Sep 19, 2015 10:11:57 AM com.nowjava.intro.EmployeeTest anotherTest
INFO:Q: employee1.equals(employee2) ?A : false

关于比较 Integer 的说明

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

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

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

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

覆盖 equals()

可通过覆盖 Object.equals() 的默认行为,定义 equals() 对于应用程序对象的含义。同样,也可使用 Eclipse 完成此任务。确保 Employee 拥有 Eclipse IDE 源代码窗口中的焦点,然后转到 Source > Override/ImplementMethods。此时打开图 4 中所示的对话框。

图 4. Override/Implement Methods 对话框

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() ,打开图 5 中所示的对话框,选择要在 hashCode()equals() 方法中包含哪些属性。

图 5. Generate hashCode() and equals() 对话框

Generate hashCode() and equals() 对话框

选择所有属性(如图 5 所示)后单击 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() 方法看起来很复杂,但它的操作很简单:如果传入的对象与清单 8 中的对象相同,则 equals() 返回 true。如果传入的对象为 null(表示缺失),则返回 false

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

练习

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

<h4 id=”练习” 1-为-code-employee-code-生成一个-code-equals-code>练习 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.nowjava.intro.EmployeeTest yetAnotherTest
INFO:Q: employee1 == employee2?      A: false
Sep 19, 2015 11:27:23 AM com.nowjava.intro.EmployeeTest yetAnotherTest
INFO:Q: employee1.equals(employee2)?A: true

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

<h4 id=”练习” 2-覆盖-code-tostring-code>练习 2:覆盖 “toString()”

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

如果认为 Eclipse 可为您生成一个 toString() 方法,那就对了。返回 Project Explorer 并右键单击 Person 类,然后选择 Source > Generate toString()…。此时会看到一个类似图 5 的对话框。选择所有属性并单击 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 上拥有的变量和方法是 实例 变量和方法。要使用它们,必须实例化所需要的类,或者拥有对该实例的引用。每个对象实例都拥有变量和方法,而且对于每个实例,准确的行为会有所不同,因为这些行为基于对象实例的状态。

展开阅读全文