構造器,可以定義對象的初始狀態,不僅如此,由於對象構造非常重要,所以Java提供了多種編寫構造器的機制。
重載
有些類有多個構造器。例如,可以構造一個空的StringBuilder對象:
StringBuilder message = new StringBuilder();
或者,可以指定一個初始化字符串:
StringBuilder todoList = new StringBuilder("To do:\n");
這種特徵就叫做重載(overloading)。如果多個方法(比如,StringBuilder構造器方法)有相同的名字、不同的參數,便產生了重載。編譯器必須挑選出具體執行哪個方法,它通過用各個方法給出的參數類型與特定方法調用所使用的值類型進行匹配來挑選出相應的方法。
- Java允許重載任何方法,而不只是構造器方法。
- 不能有兩個名字相同、參數類型也相同,返回值類型不同的方法。
默認域初始化
如果在構造器中沒有顯式地給域賦予初值,那麼就會被自動地賦爲默認值:數值爲0、布爾值爲false、對象引用爲null。如果不明確地對域進行初始化,就會影響程序代碼的可讀性。
如Employee類,假定沒有在構造器中對某些域進行初始化,就會默認地將salary域初始化爲0,將name和hireDay域初始化爲null。
如果此時調用getName方法或getHireDay方法,則會得到一個null引用,這不是我們所希望的結果,也會產生很多的Bug:
LocalDate h = harry.getHireDay();
int year = h.getYear(); // 這裏會出錯,因爲變量h是null的引用,null沒有getYear()方法。
無參數的構造器
很多類都包含一個無參數的構造函數,對象由無參數構造函數創建時,其狀態會設置爲適當的默認值。如:Employee類的無參數構造函數:
public Employee() {
name = "";
salary = 0;
hireDay = LocalDate.now();
}
如果在編寫一個類時沒有編寫構造器,那麼系統就會提供一個無參數構造器。這個構造器將所有的實例域設置爲默認值。於是,實例域中的數值型數據設置爲0、布爾型數據設置爲false、所有對象變量將設置爲null。
如果類中提供了至少一個構造器,但是沒有提供無參數的構造器,則在構造對象時如果沒有提供參數會被視爲不合法。如,在Employee類中提供了帶參數的構造器:
Employee(String name, double salary, int y, int d)
對於這個類,由於提供了一個帶參的構造器,再調用默認構造器的話屬於不合法,也就是,調用
e = new Employee();
將會產生錯誤。
- 僅當類沒有提供任何構造器的時候,系統纔會提供一個默認的構造器。
顯式域初始化
通過重載類的構造器方法,可以採用多種形式設置類的實例域的初始狀態。確保不管怎麼調用構造器,每個實例域都可以被設置爲一個有意義的處置,這是一種很好的設計習慣。
可以在類定義中,直接將一個值賦給任何域。如:
class Employee{
private String name = "":
}
在執行構造器之前,先執行賦值操作。當一個類的所有構造器都希望把相同的值賦予某個特定的實例域時,這種方式特別有用。
初始值不一定是常量值。如,可以通過調用方法對域進行初始化:
class Employee {
private static int nextId;
private int id = assignId();
private static int assignId() {
int r = nextId;
nextId++;
return r;
}
}
參數名
在編寫很小的構造器時,常常在參數命名上出現各種問題。
通常,參數用單個字符命名(圖方便):
public Employee(String n, double s) {
name = n;
salary = s;
}
這樣做沒有錯誤,但是很不友好(對人不友好),也相當於一個缺陷:只有在閱讀代碼才能瞭解參數n和參數s的含義。
有些程序員在每個參數前面加上一個無意義的前綴“a”:
public Employee(String aName, String aSalary) {
name = aName;
salary = aSalary;
}
這樣就比單個字符的好了很多,很清晰,在看到參數名的時候就知道參數的含義。
還有一種常用的技巧(非常常用),它基於這樣的事實:參數變量用同樣的名字將實例域屏蔽起來。如:如果將參數命名爲salary,salary將引用這個參數,而不是實例域。但是,可以採用this.salary的形式訪問實例域。如:
public Employee(String name, String salary) {
this.name = name;
this.salary = salary;
}
調用另一個構造器
關鍵字this應用方法的隱式參數。這個關鍵字還有另外一個含義。
如果構造器的第一個語句形如this(…),這個構造器將調用同一個類的另一個構造器。如:
public Employee(double s) {
// 調用另外一個構造器Employee(String, double)
this("Employee #" + nextId, s);
nextId++;
}
當調用Employee(double)構造器的時候,將先調用Employee(String, double)構造器。
採用這種方式使用this關鍵字非常有用,這樣對公共的構造器代碼只編寫一次即可。
初始化塊
我們已經接觸了兩種初始化數據域的方法:
- 在構造器中設置值
- 在聲明中賦值
實際上,Java還有第三種機制,稱爲初始化塊(initialization block)。在一個類的聲明中,可以包含多個代碼塊。只要構造類的對象,這些塊就會被執行。如:
class Employee {
private static int nextId;
private int id;
private String name;
private double salary;
// 初始化塊
{
id = nextId;
nextId++;
}
// 構造器
public Employee(String name, String salary) {
this.name = name;
this.salary = salary;
}
...
}
在這個例子中,無論使用哪個構造器構造對象,id域都在對象初始化塊中被初始化。首先運行初始化塊,然後才運行構造器的主體部分。
這種機制不是必須的,也不常見(非常的不常見)。通常會直接將初始化代碼放在構造器中。
由於初始化數據有多種途徑,所以列出構造過程的所有路徑可能相當混亂。下面是調用構造器的具體處理步驟:
- 所有數據域被初始化爲默認值(0、false、null)。
- 按照在類聲明中出現的次序,依次執行所有域初始化語句和初始化塊。
- 如果構造器第一行調用了第二個構造器,則執行第二個構造器主體。
- 執行這個構造器主體。
可以通過提供一個初始化值,或者使用一個靜態的初始化塊來對靜態域進行初始化。如:
// 初始化值
private static int nextId = 1;
如果對類的靜態域進行初始化的代碼比價複雜,那麼可以使用靜態的初始化塊,將代碼放在一個塊中,並標記關鍵字static,如:
// 靜態初始化塊,將僱員的ID初始賦予一個小於10000的隨機整數
static {
Random generator = new Randow();
nextId = generator.nextInt(10000);
}
在類第一次加載的時候,將會進行靜態域的初始化。與實例域一樣,除非將它們顯式地設置爲其他值,否則默認的初始值是0、false、null。所有的靜態初始化語句以及靜態初始化塊都將依照類定義的順序執行。
捐贈
若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。