通常,在实际情况下编写的类中,有自己的实例域(变量)和实例方法。想创建一个完整的程序,应该将若干个类组合在一起,其中只有一个类有 main 方法,作为启动项。

Employee 类

先来编写一个功能非常简单的雇员类。
EmployeeTest.java

  1. import java.time.*;
  2. public class EmployeeTest
  3. {
  4. public static void main(String[] args)
  5. {
  6. // fill the staff array with three Employee objects
  7. Employee[] staff = new Employee[3];
  8. staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
  9. staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
  10. staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
  11. // raise everyone's salary by 5%
  12. for (Employee e : staff)
  13. e.raiseSalary(5);
  14. // print out information about all Employee objects
  15. for (Employee e : staff)
  16. System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay());
  17. }
  18. }
  19. class Employee
  20. {
  21. private String name;
  22. private double salary;
  23. private LocalDate hireDay;
  24. public Employee(String n, double s, int year, int month, int day)
  25. {
  26. name = n;
  27. salary = s;
  28. hireDay = LocalDate.of(year, month, day);
  29. }
  30. public String getName()
  31. {
  32. return name;
  33. }
  34. public double getSalary()
  35. {
  36. return salary;
  37. }
  38. public LocalDate getHireDay()
  39. {
  40. return hireDay;
  41. }
  42. public void raiseSalary(double byPercent)
  43. {
  44. double raise = salary * byPercent / 100;
  45. salary += raise;
  46. }
  47. }

可以看到,在 main 函数中,显示构造了一个 Employee 数组,大小为 3 。
紧接着利用 Employee 类中的 raiseSalary() 来将每个雇员的薪资提升 5% 。
最后调用 getName()getSalary()getHireDay() 来打印每个雇员的信息。
可以看到,在 Employee.java 文件中有两个类:Employee 类和带有 public 访问修饰符的 EmployeeTest 类。这时因为文件名必须与 public 类的名字相匹配。在一个源文件中,只能由一个共有类,但可以有任意数目的非共有类。
当你编译 Employee.java 的时候,编译器会在当前目录下创建两个类文件:Employee.classEmployeeTest.class 。你可以运行 EmployeeTest 来启动程序,因为该类中包含 main 方法。

多个源文件的使用

在上述中,一个源文件包含了两个类。但好的方法是在将每个类存放在单独的源文件中。Employee 类对应 Employee.java 文件,EmployeeTest 类对应 EmployeeTest.java 文件。
所以,你要编译源程序,你可以使用通配符:

  1. $ javac Employee*.java

当然还有更好的方式:

  1. $ javac EmployeeTest.java

Java 编译器发现 EmployeeTest.java 使用了 Employee 类,就会查找名为 Employee.class 的文件。如果没有找到这个文件,就会自动搜索 Employee.java ,然后对它进行编译。更智能的是,如果 Employee.java 版本比已有的 Employee.class 文件版本新,Java 编译器就会自动地重新编译这个文件。上述查找过程遵循包的规则,上述代码中没有指定包,他们属于默认包。

剖析 Employee 类

Employee 类的实例中有三个实例域来存放将要操作的数据:

  1. private String name;
  2. private double salary;
  3. private LocalDate hireDay;

private 确保只有 Employee 类自身的方法能够访问这些实例域。

不提倡用 public 来标记实例域,这样会破坏封装

构造器:

  1. public Employee(int n, double s, int year, int month, int day) {
  2. name = n;
  3. salary = s;
  4. hireDay = LocalDate.of(year, month, day);
  5. }

构造器与类同名,并总是伴随着 new 操作符的执行被调用。
关于构造器:

  • 构造器与类同名
  • 每个类可以有一个及其以上的构造器
  • 构造器可以有 0 个、1 个或多个参数
  • 构造器没有返回值
  • 构造器总是伴随着 new 操作符一起调用

    隐式参数与显示参数

    Employee 类中有一个提升工资的方法: ``` public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; }
  1. `raiseSalary()` 有两个参数。第一个参数称为隐式(implicit)参数,是出现在方法名前的 Employee 类对象,在方法中,用关键字 this 表示隐式参数。第二个参数位于方法名后面括号中的数值,这是一个显式(explicit)参数。(有些人把隐式参数称为方法调用的目标或接收者。)<br />将上述方法改成用 this 表示的隐私参数:

public void raiseSalary(double byPercent) { double raise = this.salary * byPercent / 100; this.salary += raise; }

  1. ### 封装的优点
  2. 看看 `Employee` 类中的 `getName()`

public String getName() { return name; // or return this.name; }

  1. 上述 `getName()` 是典型的访问器方法。由于它们只返回实例域值,因此又称为域访问器。<br />你可能在想,将 name 域标记为 public,是不是更好?<br />关键在于 name 是一个**只读域**。一旦在构造器中设置完毕,就没有任何一个办法可以对它进行修改,这样来确保 name 域不会受到外界的破坏。而 public 没有这个功能。<br />虽然 salary 不是只读域,但是它只能用 `raiseSalary()` 修改。特别是一旦这个域值出现了错误,只要调试这个方法就可以了。如果 salary 域是 public 的,破坏这个域值的捣乱者有可能会出没在任何地方。<br />所以对于实例域的值,应该满足下面三项:
  2. - 一个私有的数据域;
  3. - 一个共有的域访问器方法;
  4. - 一个共有的域更改器方法;
  5. 虽然这让程序设计更复杂,但是好处也很明显:<br />首先,可以改变内部实现,除了该类的方法之外,不会影响其他代码。<br />例如,将上述的名字的域改为:

private String firstName; private String lastName;

  1. 那么 `getName()` 可以改为:

firstName + “ “ + lastName

  1. 另一个好处是:更改器方法可以执行错误检查,然而直接对域进行赋值将不会进行这些处理。<br />例如,`setSalary()` 可以检查薪金是否小于 0。<br />另外,不要编写 返回 引用**可变对象** 的**访问器**方法。(更改器还是可以)<br />例如,如果在 `Employee` 类中,将 hireDay 域设置为 Date 类。

class Employee { private Date hireDay; … public Date getHireDay() { return hireDay; // Bad } … }

  1. 然而,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

  1. 解决方法为:不要返回可变对象的引用,应该对它进行克隆:

class Employee { … public Date getHireDay() { return (Date) hireDay.clone(); // OK } … }

  1. ### 基于类的访问权限*
  2. 从前面已经知道,方法可以访问所调用对象的私有数据。一个方法可以访问**所属类**的**所有对象**的**私有数据**。比如:

class Employee { … public boolean equals(Employee other) { return name.equals(other.name); // 注意这里的 other.name } }

  1. 调用方式为:

if (harry.equals(boss)) …

  1. 上述调用方式,访问了 harry 的私有域(name)。然而,它还访问了 boss 的私有域(name)。这是合法的,其原因是 boss Employee 类对象,而 Employee 类的方法可以访问 Employee 类的任何一个对象的私有域。
  2. ### 私有方法
  3. 将一个计算代码划分成若干个独立的辅助方法。通常,这些辅助方法不应该成为公有接口的一部分,这是由于它们往往与当前的实现机制非常紧密,或者需要一个特别的协议以及一个特别的调用次序。最好将这样的方法设计为 private 的。<br />对于私有方法,如果改用其他方法实现相应的操作,则不必保留原有的方法。如果数据的表达方式发生了变化,这个方法可能会变得难以实现,或者不再需要。然而,只要方法是私有的,类的设计者就可以确信:它不会被外部的其他类操作调用,可以将其删去。如果方法是公有的,就不能将其删去,因为其他的代码很可能依赖它。
  4. ### final 实例域
  5. 可以将实例域定义为 final。构建对象时**必须**初始化这样的域。也就是说,必须确保在**每一个**构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。<br />final 修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。<br />对于可变的类,使用 final 修饰符可能会造成混乱。例如:

private final StringBuilder evaluations;

  1. 在构造器中初始化为:

evaluations = new StringBuilder();

  1. final 关键字只是表示存储在 evaluations 变量中的对象**引用**不会再指示其他 StringBuilder 对象。不过这个对象可以更改:

public void giveGoldStar() { evaluations.append(LocalDate.now() + “: Gold star!\n”); }

  1. 上述可能已经讲的很清楚了,但我还想来讲两句:<br />当一个变量指定为 final ,表示该变量不会在改变了。<br />对于基本类型,就相当于常量。<br />对于对象,如果是不可变对象,也可以看成常量。<br />但,如果是可变对象,就不一样了。该变量不能赋值,但是你可以修改该变量指向对象的域。<br />例如:你创建了一个 final Emloyee 变量:

final Employee yikang = new Employee(“Yi Kang”, 1000, 1998, 9, 19);

`` 假设 yikang 这个变量指向 460141958 这个内存。那么现在从始至终,该变量的指向都不会改变。但是在这个内存中的Employee` 实例确实可以变化的(前提是可变对象)。因为它的变化不会改变 yikang 指向 460141958 ,并且也改变不了。