前面,我們已經編寫了一些簡單的類,但是,哪些類都只包含了一個簡答的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修飾符只是表示存儲在變量中的對象不會再指示其他的對象,不過這個對象內容是可能更改的(這個以後慢慢理解,但要記住這一條)。
捐贈
若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。