通常,在实际情况下编写的类中,有自己的实例域(变量)和实例方法。想创建一个完整的程序,应该将若干个类组合在一起,其中只有一个类有 main 方法,作为启动项。
Employee 类
先来编写一个功能非常简单的雇员类。
EmployeeTest.java
import java.time.*;
public class EmployeeTest
{
public static void main(String[] args)
{
// fill the staff array with three Employee objects
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("Tony Tester", 40000, 1990, 3, 15);
// raise everyone's salary by 5%
for (Employee e : staff)
e.raiseSalary(5);
// print out information about all Employee objects
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;
}
}
可以看到,在 main 函数中,显示构造了一个 Employee
数组,大小为 3 。
紧接着利用 Employee
类中的 raiseSalary()
来将每个雇员的薪资提升 5% 。
最后调用 getName()
、getSalary()
和 getHireDay()
来打印每个雇员的信息。
可以看到,在 Employee.java 文件中有两个类:Employee
类和带有 public 访问修饰符的 EmployeeTest
类。这时因为文件名必须与 public 类的名字相匹配。在一个源文件中,只能由一个共有类,但可以有任意数目的非共有类。
当你编译 Employee.java 的时候,编译器会在当前目录下创建两个类文件:Employee.class 和 EmployeeTest.class 。你可以运行 EmployeeTest
来启动程序,因为该类中包含 main 方法。
多个源文件的使用
在上述中,一个源文件包含了两个类。但好的方法是在将每个类存放在单独的源文件中。Employee
类对应 Employee.java 文件,EmployeeTest
类对应 EmployeeTest.java 文件。
所以,你要编译源程序,你可以使用通配符:
$ javac Employee*.java
当然还有更好的方式:
$ javac EmployeeTest.java
Java 编译器发现 EmployeeTest.java 使用了 Employee
类,就会查找名为 Employee.class 的文件。如果没有找到这个文件,就会自动搜索 Employee.java ,然后对它进行编译。更智能的是,如果 Employee.java 版本比已有的 Employee.class 文件版本新,Java 编译器就会自动地重新编译这个文件。上述查找过程遵循包的规则,上述代码中没有指定包,他们属于默认包。
剖析 Employee 类
Employee 类的实例中有三个实例域来存放将要操作的数据:
private String name;
private double salary;
private LocalDate hireDay;
private 确保只有 Employee
类自身的方法能够访问这些实例域。
不提倡用 public 来标记实例域,这样会破坏封装
构造器:
public Employee(int n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
构造器与类同名,并总是伴随着 new 操作符的执行被调用。
关于构造器:
- 构造器与类同名
- 每个类可以有一个及其以上的构造器
- 构造器可以有 0 个、1 个或多个参数
- 构造器没有返回值
- 构造器总是伴随着 new 操作符一起调用
隐式参数与显示参数
在Employee
类中有一个提升工资的方法: ``` public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
`raiseSalary()` 有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的 Employee 类对象,在方法中,用关键字 this 表示隐式参数。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参数。(有些人把隐式参数称为方法调用的目标或接收者。)<br />将上述方法改成用 this 表示的隐私参数:
public void raiseSalary(double byPercent) { double raise = this.salary * byPercent / 100; this.salary += raise; }
### 封装的优点
看看 `Employee` 类中的 `getName()`:
public String getName() { return name; // or return this.name; }
上述 `getName()` 是典型的访问器方法。由于它们只返回实例域值,因此又称为域访问器。<br />你可能在想,将 name 域标记为 public,是不是更好?<br />关键在于 name 是一个**只读域**。一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样来确保 name 域不会受到外界的破坏。而 public 没有这个功能。<br />虽然 salary 不是只读域,但是它只能用 `raiseSalary()` 修改。特别是一旦这个域值出现了错误,只要调试这个方法就可以了。如果 salary 域是 public 的,破坏这个域值的捣乱者有可能会出没在任何地方。<br />所以对于实例域的值,应该满足下面三项:
- 一个私有的数据域;
- 一个共有的域访问器方法;
- 一个共有的域更改器方法;
虽然这让程序设计更复杂,但是好处也很明显:<br />首先,可以改变内部实现,除了该类的方法之外,不会影响其他代码。<br />例如,将上述的名字的域改为:
private String firstName; private String lastName;
那么 `getName()` 可以改为:
firstName + “ “ + lastName
另一个好处是:更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。<br />例如,`setSalary()` 可以检查薪金是否小于 0。<br />另外,不要编写 返回 引用**可变对象** 的**访问器**方法。(更改器还是可以)<br />例如,如果在 `Employee` 类中,将 hireDay 域设置为 Date 类。
class Employee { private Date hireDay; … public Date getHireDay() { return hireDay; // Bad } … }
然而,Date 类有一个更改器方法 `setTime()` ,可以设置毫秒数:
Employee harry = …; Date d = harry.getHireDay(); double tenYearsInMilliSeconds = 10 365.25 24 60 60 * 1000; d.setTime(d.getTime() - (long)tenYearsInMilliSeconds); // 破坏了封装性 // let’s give Harry ten years of added seniority
解决方法为:不要返回可变对象的引用,应该对它进行克隆:
class Employee { … public Date getHireDay() { return (Date) hireDay.clone(); // OK } … }
### 基于类的访问权限*
从前面已经知道,方法可以访问所调用对象的私有数据。一个方法可以访问**所属类**的**所有对象**的**私有数据**。比如:
class Employee { … public boolean equals(Employee other) { return name.equals(other.name); // 注意这里的 other.name } }
调用方式为:
if (harry.equals(boss)) …
上述调用方式,访问了 harry 的私有域(name)。然而,它还访问了 boss 的私有域(name)。这是合法的,其原因是 boss 是 Employee 类对象,而 Employee 类的方法可以访问 Employee 类的任何一个对象的私有域。
### 私有方法
将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为 private 的。<br />对于私有方法,如果改用其他方法实现相应的操作,则不必保留原有的方法。如果数据的表达方式发生了变化,这个方法可能会变得难以实现,或者不再需要。然而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的,就不能将其删去,因为其他的代码很可能依赖它。
### final 实例域
可以将实例域定义为 final。构建对象时**必须**初始化这样的域。也就是说,必须确保在**每一个**构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。<br />final 修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。<br />对于可变的类,使用 final 修饰符可能会造成混乱。例如:
private final StringBuilder evaluations;
在构造器中初始化为:
evaluations = new StringBuilder();
final 关键字只是表示存储在 evaluations 变量中的对象**引用**不会再指示其他 StringBuilder 对象。不过这个对象可以更改:
public void giveGoldStar() { evaluations.append(LocalDate.now() + “: Gold star!\n”); }
上述可能已经讲的很清楚了,但我还想来讲两句:<br />当一个变量指定为 final ,表示该变量不会在改变了。<br />对于基本类型,就相当于常量。<br />对于对象,如果是不可变对象,也可以看成常量。<br />但,如果是可变对象,就不一样了。该变量不能赋值,但是你可以修改该变量指向对象的域。<br />例如:你创建了一个 final Emloyee 变量:
final Employee yikang = new Employee(“Yi Kang”, 1000, 1998, 9, 19);
``
假设 yikang 这个变量指向 460141958 这个内存。那么现在从始至终,该变量的指向都不会改变。但是在这个内存中的
Employee` 实例确实可以变化的(前提是可变对象)。因为它的变化不会改变 yikang 指向 460141958 ,并且也改变不了。