在本节中,我们继续将 Person
构建为 Java 应用程序。在此过程中,您可更好地了解一个对象或对象集合如何演变成应用程序。
Java 应用程序入口点
所有 Java 应用程序都需要一个入口点,这样 Java 运行时才能知道从这里开始执行代码。这个入口点就是 main()
方法。 域对象 —— 也就是说,作为应用程序 业务域 一部分的对象(例如 Person
和 Employee
)—— 通常没有 main()
方法,但每个应用程序至少有一个类拥有该方法。
如您所知Person
及其 Employee
子类在概念上是人力资源应用程序的一部分。现在,您将向应用程序添加一个新类,以给它一个入口点。
创建一个 Driver 类
顾名思义, Driver 类 的用途是”驱动”一个应用程序。注意,人力资源应用程序的这个简单 Driver 包含一个 main()
方法:
package com.nowjava.intro;
public class HumanResourcesApplication {
public static void main(String[] args) {
}
}
在 Eclipse 中创建 Driver 类的过程与创建 Person
和 Employee
的过程相同。将该类命名为 HumanResourcesApplication
,确保选择了相关的选项,向该类添加一个 main()
方法。Eclipse 将为您生成该类。
然后,将一些代码添加到新的 main()
方法中,使其类似于下面这段代码:
package com.nowjava.intro;
import java.util.logging.Logger;
public class HumanResourcesApplication {
private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
public static void main(String[] args) {
Employee e = new Employee();
e.setName("J Smith");
e.setEmployeeNumber("0001");
e.setTaxpayerIdentificationNumber("123-45-6789");
e.setSalary(BigDecimal.valueOf(45000.0));
e.printAudit(log);
}
}
现在,启动 HumanResourcesApplication
类并观察它的运行情况。您应看到此输出(反斜杠表示一个连续行):
Sep 19, 2015 7:59:37 PM com.nowjava.intro.Person printAudit
INFO:Name=J Smith,Age=0,Height=0,Weight=0,EyeColor=null,Gender=null\
TaxpayerIdentificationNumber=123-45-6789,EmployeeNumber=0001,Salary=45000.00
这就是创建一个简单 Java 应用程序的全部过程。在下一部分中,将介绍一些可帮助您开发更复杂应用程序的语法和库。
继承
您在本x系列中已遇到过多个继承示例。本节将更详细地解释继承的工作原理 — 包括继承分层结构、构造函数和继承,以及继承抽象。
继承的工作原理
Java 代码中的各个类位于分层结构中。分层结构中一个给定类上方的类是该类的 超类。这个特定的类是该分层结构更高层中每个类的 子类。子类继承其超类的属性和行为。java.lang.Object
类位于类分层结构的顶部,意味着每个 Java 类都是 Object
的子类并继承其属性和行为。
例如,假设有一个与清单 14 的内容类似的 Person
类。
清单 14. 公共 Person
类
public class Person {
public static final String STATE_DELIMITER = "~";
public Person() {
// Default constructor
}
public enum Gender {
MALE,
FEMALE,
UNKNOWN
}
public Person(String name, int age, int height, int weight, String eyeColor, Gender gender) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
this.eyeColor = eyeColor;
this.gender = gender;
}
private String name;
private int age;
private int height;
private int weight;
private String eyeColor;
private Gender gender;
清单 14 中的 Person
类隐式地继承了 Object
。因为我们会假定每个类都继承 Object
,所以不需要为您定义的每个类键入 extends Object
。但说一个类继承它的超类是什么意思?它表示 Person
能够访问其超类中已公开的变量和方法。在本例中,Person
可查看和使用 Object
的公共方法和变量,以及 Object
的受保护方法和变量。
定义一个类分层结构
现在假设有一个继承 Person
的 Employee
类。Employee
的类定义(或 继承图 )类似于这样:
public class Employee extends Person {
private String taxpayerIdentificationNumber;
private String employeeNumber;
private BigDecimal salary;
// ...
}
Employee
继承图暗示,Employee
能够访问 Person
中的所有公共和受保护变量和方法(因为 Employee
直接扩展 Person
),以及 Object
中的公共和受保护变量和方法(因为 Employee
实际上也扩展了 Object
,尽管是间接扩展)。但是,因为 Employee
和 Person
都位于同一个包中,所以 Employee
也能访问 Person
中的 package-private (有时称为 友好 )变量。
要想深入了解类分层结构,可创建第三个扩展 Employee
的类:
public class Manager extends Employee {
// ...
}
在 Java 语言中,任何类可拥有最多 1 个超类,但一个类可拥有任意个子类。这是 Java 语言继承分层结构方面要记住的最重要一点。
单继承与多继承
像 C++ 这样的语言支持 多继承 的概念:在层次结构中的任何一点,一个类可以直接从一个或多个类继承。Java 语言只支持 单继承 — 这意味着只能使用单个类的 extends
关键字。因此,任何 Java 类的类层次结构始终包含一条直线,一直到 java.lang.Object
。但是,正如您将在下一个主要部分 —接口中学习的那样,Java 语言支持在一个类中实现多个接口,提供您一种实现单一继承的的处理方法。
构造函数和继承
构造函数不是完整的面向对象成员,所以它们不是继承的;您必须在子类中明确地实现它们。介绍该主题之前,我要回顾一下有关如何定义和调用构造函数的一些基本规则。
构造函数基础知识
记住,一个构造函数的名称始终与它用于构造的类相同,而且它没有返回类型。例如:
public class Person {
public Person() {
}
}
每个类至少拥有一个构造函数,而且如果没有明确地为类定义构造函数,那么编译器会生成一个(称为 默认构造函数 )。前面的类定义和这个类定义具有相同的功能:
public class Person {
}
调用一个超类构造函数
要调用一个超类构造函数,而不是默认的构造函数,也必须明确地这么做。例如,假设 Person
拥有一个构造函数,该函数仅接受所创建的 Person
对象的名称。通过 Employee
的默认构造函数,您可调用 Person
构造函数,如清单 15 所示:
清单 15. 初始化一个新的 Employee
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
super("Elmer J Fudd");
}
}
但是,您可能绝对不想以这种方式初始化一个新的 Employee
对象。更熟悉面向对象的概念和一般性的 Java 语法之前,如果认为需要超类构造函数,一个不错的想法是在子类中实现它们。清单 16 在 Employee
中定义了一个与 Person
中的构造函数相似的构造函数,以便它们匹配。从维护的角度讲,这种方法要清楚易懂得多。
清单 16. 调用一个超类
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee(String name) {
super(name);
}
}
声明一个构造函数
构造函数所做的第一件事就是调用其直接超类的默认构造函数,除非您 — 在该构造函数的第一行代码中 — 调用了一个不同的构造函数。例如,这两种声明具有相同的功能:
public class Person {
public Person() {
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
}
}
或者:
public class Person {
public Person() {
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
super();
}
}
无参数构造函数
如果您提供了一个替代的构造函数,就必须明确地提供默认构造函数;否则后者将不可用。例如,以下代码会出现编译错误:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
}
}
此示例中的 Person
类没有默认构造函数,因为它提供了一个替代性的构造函数,但没有明确地包含默认构造函数。
构造函数如何调用构造函数
一个构造函数可通过 this
关键字和一个参数列表来调用另一个类中的构造函数。像 super()
一样,this()
调用必须是构造函数中的第一行代码。例如:
public class Person {
private String name;
public Person() {
this("Some reasonable default?");
}
public Person(String name) {
this.name = name;
}
}
您会经常看到此用法,其中一个构造函数委托给另一个构造函数,如果调用后者,则传入一个默认构造函数。向一个类添加一个新构造函数,同时最大限度减少对已使用旧构造函数的代码的影响,这也是一种不错的方式。
构造函数访问级别
构造函数可拥有您想要的任何访问级别,并且会应用一些可见性规则。表 1 总结了构造函数访问规则。
表 1. 构造函数访问规则
构造函数访问修饰符 | 描述 |
---|---|
public | 任何类都可调用构造函数。 |
protected | 只能由同一个包或任何子类中的类调用构造函数。 |
无修饰符( package-private ) | 同一个包中的任何类都可调用构造函数。 |
private | 只能由定义构造函数的类调用该构造函数。 |
您可能想到了将构造函数声明为 protected
甚至是 package-private 的用例,不过 private
构造函数有何用处?比如说在实现 工厂模式 时,我不允许通过 new
关键字直接创建对象,此时就使用了私有构造函数。在这种情况下,我会使用一个静态方法来创建类的实例,而且该方法 — 包含在该类中 — 将允许调用这个私有构造函数。
继承和抽象
如果一个子类覆盖了超类中的一个方法,该方法在本质上是隐藏的,因为通过对子类的引用来调用它时,会调用该方法的子类版本,而不是超类版本。这并不是说,无法再访问超类方法。子类可在方法名称前添加 super
关键字来调用超类方法(与构造函数规则不同,这可从子类方法的任何行中执行,甚至可在一个不同的方法内执行)。默认情况下,如果通过对子类的引用来调用子类方法,那么 Java 程序会调用它。
这同样适用于变量,前提是调用方能够访问该变量(也就是变量对尝试访问它的代码是可见的)。随着您逐渐精通 Java 编程,此细节可能会让您非常伤心。但是,Eclipse 提供了大量的警告来提示您正在隐藏来自超类的变量,或者方法调用没有调用您认为它应调用的实体。
在 OOP 上下文中, 抽象 是指将数据和行为一般化为某种类型,这种类型在继承分层结构中比当前类具有更高的层级。将变量或方法从一个子类移动到一个超类时,就可以说是在 抽象化 这些成员。这么做的主要目的是,通过将通用的代码推送到分层结构中尽可能高的层级,从而重用这些代码。将通用的代码放在一个位置也更容易维护。
抽象类和方法
有时,您希望创建一些仅用作抽象,而不是必需实例化的类。这些类称为 抽象类。出于同样的原因,有时需要以不同的方式为每个实现超类的子类实现某些方法。这些方法是 抽象方法。以下是抽象类和抽象方法的一些基本规则:
- 可将任何类声明为
abstract
。 - 无法实例化抽象类。
- 抽象方法不能包含一个方法主体。
- 任何包含抽象方法的类都必须声明为
abstract
。
使用抽象
假设您不允许直接实例化 Employee
类。使用 abstract
关键字声明它即可:
public abstract class Employee extends Person {
// etc.
}
如果尝试运行此代码,会出现一个编译错误:
public void someMethodSomwhere() {
Employee p = new Employee();// compile error!!
}
编译器抱怨 Employee
是抽象的,因此无法实例化。