前面,我们已经编写了一些简单的类,但是,哪些类都只包含了一个简答的main方法。现在我们开始学习如何设计复杂应用程序所需要的各种主力类(workhorse class)。通常,这些类没有main方法,却有自己的实例域和实例方法。想要创建一个完整的程序,应该将若干类组合在一起,其中只有一个类有main方法。
Employee类
在Java中,最简单的类定义形式为:
class ClassName {
field1;
field2;
...
constructor1;
constructor2;
...
method1;
method2;
}
我们看一个非常简单的Employee类:
class Employee {
// 实例域
private String name;
private double salary;
private LocalDate hireDAy;
// 构造器
public Employee(String n, double s, int year, int month, int day) {
name = n;
salay = s;
hireDay = LocalDate.of(year, month, day);
}
// 方法
public String getName() {
return name;
}
...
}
这就是一个类的几个部分,分别为实例域、构造器、方法,我们看一下这个类的实际应用:
import java.time.*;
// 测试Employee类
public class EmployeeTest {
public static void main(String[] args) {
// 定义员工数组,添加3个员工
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Carl Cracker", 40000, 1990, 3, 15);
// 将每个人的工资提升5%
for (Employee e : staff)
e.raiseSalary(5);
// 打印员工信息
for (Employee e : staff)
System.out.println("name = " + e.getName() + ", salary = " + e.getSalary() + ", hireDay = " + e.getHireDay());
}
}
class Employee {
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
}
在这个程序中,构造了一个Employee数组,并填入了三个雇员对象:
Employee[] staff = new Employee[3];
staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Carl Cracker", 40000, 1990, 3, 15);
接下来,利用Employee类的raiseSalary方法将每个雇员的薪水提高5%:
for (Employee e : staff)
e.raiseSalary(5);
最后,调用getName方法、getSalay方法和getHireDay方法将每个雇员的信息打印出来:
for (Employee e : staff)
System.out.println("name = " + e.getName() + ", salary = " + e.getSalary() + ", hireDay = " + e.getHireDay());
}
- **注意,这个示例程序中包含两个类:Employee类和带有public访问修饰符的EmployeeTest类。**EmployeeTest类中包含了main方法。
源文件名是EmployeeTest.java,这是因为文件名必须与public类的名字相匹配。在一个源文件中,只能有一个公有类,但可以有任意数目的非公有类。
接下来,当编译这段源代码的时候,编译器将在目录下创建两个类文件:EmployeeTest.class和Employee.class。
将程序中包含main方法的类名提供给字节码解释器,以便启动这个程序:
java EmployeeTest
字节码解释器开始运行EmployeeTest类的main方法中的代码。在这段代码中,先后构造了三个新Employee对象,并显示它们的状态。
多个源文件的使用
在上面的示例中,一个源文件包含了两个类。许多程序员习惯将每一个类存在一个单独的源文件中。例如:将Employee类放在文件Employee.java中,将EmployeeTest类存放在文件EmployeeTest.java中。
如果喜欢这样组织文件,将可以有两种编译源程序的方法。一种是使用通配符调用Java编译器:
javac Employee*.java
于是,所有与通配符匹配的源文件都被编译成类文件。或者键入下列命令:
javac EmployeeTest.java
你可能会感到惊讶,使用第二种方式,并没有显示地编译Employee.java。然而,当Java编译器发现EmployeeTest.java使用了Employee时会查找名为Employee.class文件。如果没有找到这个文件,就会自动地搜索Employee.java,然后,对它进行编译。更重要的是:如果Employee.java版本较以后的Employee.class文件版本新,Java编译器就会自动地重新编译这个文件。
剖析Employee类
首先从这个类的方法开始,这个类包含一个构造器和4个方法:
public Employee(String n, double s, int year, int month, int day){}
public String getName() {}
public double getSalary() {}
public LocalDate getHireDay() {}
public void raiseSalary(double byPercent) {}
这个类的所有方法都被标记为public。关键字public意味着任何类的任何方法都可以调用这些方法。
接下来,需要注意在Employee类的实例中有三个实例域用来存放将要操作的数据:
private String name;
private double salary;
private LocalDate hireDay;
关键字private确保只有Employee类自身的方法能够访问这些实例域,而其他类的方法不能读写这些域。
最后,请注意,有两个实例域本身就是对象:name域是String类对象,hireDay域是LocalDate类对象。这种情形十分常见:类通常包括类型属于某个类类型的实例域。
构造器
Employee类的构造器如下:
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
可以看到,构造器与类同名。在构造Employee类的对象时,构造器会运行,一遍将实例域初始化为所希望的状态。
如,当使用下面这条代码创建Employee类实例时:
new Employee("James Bond", 100000, 1950, 1, 1);
将会把实例域设置为:
name = "James Bond";
salary = 100000;
hireDay = LocalDate(1950, 1, 1);
构造器与其他的方法有一个重要的不同。构造器总是伴随着new操作符的执行被调用,而不能对一个已经存在的对象调用构造器来达到重新设置实例域的目的。如
james.Employee("James Bond", 100000, 1950, 1, 1); // 这样是错误的
现在要记住关于构造器的以下几点:
- 构造器与类同名
- 每个类可以有一个以上的构造器
- 构造器可以有0个、1个或多个参数
- 构造器没有返回值
- 构造器总是伴随着new操作一起调用
隐式参数和显示参数
方法用于操作对象以及存取它们的实例域,如:
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
将调用这个方法的对象的salary实例域设置为新值,如:
number007.raiseSalary(5);
它的结果将number007.salary域的值增加5%。具体地说,这个调用将执行下列指令:
double raise = number007.salary * 5 / 100;
number007.salary += raise;
raiseSalary方法有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的Employee类对象。第二个参数位于方法名后面括号中的数值,这是一个显示(explicit)参数。(有些人把隐式参数称为方法调用的目标或接收者。)
可以看到,显式参数是明显地列在方法声明中的,如double byPercent。隐式参数没有出现在方法声明中。
在每一个方法中,关键字this表示隐式参数。如果需要的话,可以用下列方式编写raiseSalary方法:
public void raiseSalary(double byPercent) {
double raise = this.salary * byPercent / 100;
this.salary += raise;
}
有些程序员更偏爱这样的风格,因为这样可以将实例域与局部变量明显地区分开来。
封装的优点
再仔细看一下非常简单的getName、getSalary、getHireDay方法。
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public LocalDate getHireDay() {
return hireDay;
}
这些都是典型的访问器方法。由于它们只返回实例域值,因此又称为域访问器。
那么将name、salary和hireDay域标记为public,以此来取代独立的访问器方法会不会更容易些呢?
关键在于name是一个只读域。一旦在构造器中设置完毕,就没有任何一个方法可以对它进行修改,这样来确保name域不受到外界的破坏。
虽然salary不是只读域,但是它只能用raiseSalary方法进行修改。特别是一旦这个域值出现了错误,只要调试这个方法就可以了。如果salary域是public的,破坏这个域值的捣乱者有可能会出现在任何地方。
在有些是有,需要获得或设置实例域的值。因此,应该提供下面三项内容:
- 一个私有的数据域
- 一个共有的域访问器方法
- 一个共有的域更改器方法
这样做要比提供一个简单的共有数据域复杂些,但是却有着明显的好处:
- 可以改变内部实现,除了该类的方法之外,不会影响其他的代码。
- 更改器或访问器方法可以做一些工作。如:更改器可以执行错误检查,raiseSalary方法可以检查薪金是否小于0等。
基于类的访问权限
方法可以访问所调用对象的私有数据。一个方法可以访问所属类的所有对象的私有数据。这令很多人感到奇怪,但也很简单,如,用来比较两个雇员的equals方法:
class Employee {
public boolean equals(Employee other) {
return name.equals(other.name);
}
}
典型的调用方式是:
if (harry.equals(boss)) ...
这个方法访问harry的私有域,这点并不会让人奇怪,然而,它还访问了boss的私有域。这是合法的,其原因是boss是Employee类对象,而Employee类的方法可以访问Employee类的任何一个对象的私有域。
私有方法
在实现一个类时,由于共有数据非常危险,所以应该将所有的数据域都设置为私有的。然而,方法又应该如何设计呢?尽管绝大多数都被设计为共有的,但在某些特殊情况下,也可能将它们设计为私有的。有时,可能希望将一个计算代码划分为若干个独立的辅助方法。通常,这些辅助方法不应该成为共有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为private的。
在Java中,为了实现一个私有的方法,只需将关键字public改为private即可。
对于私有方法,如果改用其他方法实现相应的操作,则不必保留原有的方法。如果数据的表达方式发生了变化,这个方法可能会变得难以实现,或者不在需要 。然而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是共有的,就不能将其删去,因为其他的代码很可能依赖它。
final实例域
可以将实例定义为final。构建对象时必须初始化这样的域。也就是说,必须确保在每一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。
对于可变的类,使用final修饰符只是表示存储在变量中的对象不会再指示其他的对象,不过这个对象内容是可能更改的(这个以后慢慢理解,但要记住这一条)。
捐赠
若你感觉读到这篇文章对你有启发,能引起你的思考。请不要吝啬你的钱包,你的任何打赏或者捐赠都是对我莫大的鼓励。