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 对话框
输入 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 对话框
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()
方法
使用 ==
比较对象
==
语法比较对象是否相等,只有在 a
和 b
拥有相同的值时,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
,因为 integer1
和 integer2
引用了不同的对象。可将 ==
视为对”相同对象”进行一种测试。
使用 equals() 比较对象
equals()
是每种 Java 语言对象都可自由使用的方法,因为它被定义为 java.lang.Object
(每个 Java 对象都继承自该对象)的一个实例方法。
可像使用其他任何方法那样调用 equals()
:
a.equals(b);
这条语句调用对象 a
的 equals()
方法,向它传递对象 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 中,如果 ==
返回 true
,Integer
的 equals()
方法就会返回 true
,对此不应感到奇怪。但请注意第二种情况中发生的事情,在其中创建了都包含值 1
的不同对象:==
返回 false
,因为 integer1
和 integer2
引用不同的对象;但 equals()
却返回 true
。
JDK 的编写者认为,对于 Integer
, equals()
的含义与默认含义不同(回想一下,默认含义是比较对象引用,看看它们是否引用同一个对象)。对于 Integer
,在底层的 int
值相同时,equals()
返回 true
。
对于 Employee
,您没有覆盖 equals()
,所以(使用 ==
的)默认行为返回了您期望的结果,因为 employee1
和 employee2
引用不同的对象。
然后,对于所编写的任何对象,可定义适合所编写应用程序的 equals()
含义。
覆盖 equals()
可通过覆盖 Object.equals()
的默认行为,定义 equals()
对于应用程序对象的含义。同样,也可使用 Eclipse 完成此任务。确保 Employee
拥有 Eclipse IDE 源代码窗口中的焦点,然后转到 Source > Override/ImplementMethods。此时打开图 4 中所示的对话框。
图 4. Override/Implement Methods 对话框
您之前使用过这个对话框,但在本例中,我们要实现 Object.equals()
超类方法。所以在方法列表中找到要覆盖或要实现的 Object
,选择 equals(Object)
方法,然后单击 OK。Eclipse 生成正确的代码并将它放在源文件中。
如果两个 Employee
对象的状态相等,则这两个对象相等,这样就有意义了。也就是说,如果它们的值 — 姓氏、名字、年龄 — 相同,那么它们就相等。
自动生成 equals()
Eclipse 可根据您为一个类所定义的实例变量(属性)来生成一个 equals()
方法。因为 Employee
是 Person
的子类,所以首先为 Person
生成 equals()
。在 Eclipse 的 Project Explorer 视图中,右键单击 Person
并选择 Generate hashCode() and equals() ,打开图 5 中所示的对话框,选择要在 hashCode()
和 equals()
方法中包含哪些属性。
图 5. 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 中执行两个引导式练习,以便进一步完善 Person
和 Employee
。
<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()
是一个实现此目的很好的挂钩。
类成员
每个对象实例都拥有变量和方法,而且对于每个实例,准确的行为是不同的,因为它基于对象实例的状态。您在 Person
和 Employee
上拥有的变量和方法是 实例 变量和方法。要使用它们,必须实例化所需要的类,或者拥有对该实例的引用。每个对象实例都拥有变量和方法,而且对于每个实例,准确的行为会有所不同,因为这些行为基于对象实例的状态。