1. 程式人生 > >Java 構造器(定義,作用,原理)

Java 構造器(定義,作用,原理)

首先要注意的是Java的構造器並不是函式,所以他並不能被繼承,這在我們extends的時候寫子類的構造器時比較的常見,即使子類構造器引數和父類的完全一樣,我們也要寫super就是因為這個原因。
構造器的修飾符比較的有限,僅僅只有public private protected這三個,其他的例如任何修飾符都不能對其使用,也就是說構造器不允許被成名成抽象、同步、靜態等等訪問限制以外的形式。
因為構造器不是函式,所以它是沒有返回值的,也不允許有返回值。但是這裡要說明一下,構造器中允許存在return語句,但是return什麼都不返回,如果你指定了返回值,雖然編譯器不會報出任何錯誤,但是JVM會認為他是一個與構造器同名的函式罷了,這樣就會出現一些莫名其妙的無法找到構造器的錯誤,這裡是要加倍注意的。


在我們extends一個子類的時候經常會出現一些意想不到的問題,我在這裡說一些和構造器有關的。
首先說一下Java在構造例項時的順序(不討論裝載類的過程)
構造的粗略過程如下
1、分配物件空間,將空間記憶體初始化成二進位制的零,並將物件中成員初始化為0或者空,java不允許使用者操縱一個不定值的物件。
2、執行屬性值的顯式初始化(這裡有一點變化,一會解釋,但大體是這樣的)
3、執行構造器
4、將變數關聯到堆中的物件上


介紹一下準備知識,以備一會來詳細說明這個的流程
this() super()是你如果想用傳入當前構造器中的引數或者構造器中的資料呼叫其他構造器或者控制父類構造器時使用的,在一個構造器中你只能使用this()或者super()之中的一個,而且呼叫的位置只能在構造器的第一行,在子類中如果你希望呼叫父類的構造器來初始化父類的部分,那就用合適的引數來呼叫super(),如果你用沒有引數的super()來呼叫父類的構造器(同時也沒有使用this()來呼叫其他構造器),父類預設的構造器會被呼叫,如果父類沒有預設的構造器,那編譯器就會報一個錯誤,注意此處,我們經常在繼承父類的時候構造器中並不寫和父類有關的內容,此時如果父類沒有預設構造器,就會出現編譯器新增的預設構造器給你添麻煩的問題了哦。例如:Class b extends a{public b(){}}就沒有任何有關父類構造器的資訊,這時父類的預設構造器就會被呼叫。


舉個SL-275中的例子
1 public class Manager extends Employee {
2 private String department;
3
4 public Manager(String name, double salary, String dept)
{
5 super(name, salary);
6 department = dept;
7 }
8 public Manager(String n, String dept) {
9 super(name);
10 department = dept;
11 }
12 public Manager(String dept) { // 這裡就沒有super(),編譯器會自動地新增一個空引數的預設super構造器,此時如果Employee類中沒有空引數的預設構造器,那就會導致一個編譯錯誤
13 department = d;
14 }
15 }


你必須在構造器的第一行放置super或者this構造器,否則編譯器會自動地放一個空引數的super構造器的,其他的構造器也可以呼叫super或者this,呼叫成一個遞迴構造鏈,最後的結果是父類的構造器(可能有多級父類構造器)始終在子類的構造器之前執行,遞迴的呼叫父類構造器


在具體構造類例項的過程中,上邊過程的第二步和第三步是有一些變化的,這裡的順序是這樣的,分配了物件空間及物件成員初始化為預設值之後,構造器就遞迴的從繼承樹由根部向下呼叫,每個構造器的執行過程是這樣的:
1、Bind構造器的引數
2、如果顯式的呼叫了this,那就遞迴呼叫this構造器然後跳到步驟5
3、遞迴呼叫顯式或者隱式的父類構造器,除了Object以外,因為它沒有父類
4、執行顯式的例項變數初始化(也就是上邊的流程中的第二步,呼叫返回以後執行,這個步驟相當於在父構造器執行後隱含執行的,看樣子像一個特殊處理)
5、執行構造器的其它部分


這裡的步驟很重要哦!!!!!


從這個步驟中可以很明顯的發現這個例項初始化時的遞迴呼叫過程,估計看過這個你應該能夠理解這個遞迴構造鏈是怎麼樣回事了。


這裡還是給出SL-275中的一個例子,讓你充分理解一下這個遞迴的過程。


public class Object {
...
public Object() {}
...
}
public class Employee extends Object {
private String name;
private double salary = 15000.00;
private Date birthDate;
public Employee(String n, Date DoB) {
// implicit super();
name = n;
birthDate = DoB;
}
public Employee(String n) {
this(n, null);
}
}


public class Manager extends Employee {
private String department;
public Manager(String n, String d) {
super(n);
department = d;
}
}


在建立Manager("Joe Smith","Sales"):時,步驟如下
0 basic initialization
0.1 allocate memory for the complete Manager object
0.2 initialize all instance variables to their default values (0 or null)
1 call constructor: Manager("Joe Smith", "Sales")
1.1 bind constructor parameters: n="Joe Smith", d="Sales"
1.2 no explicit this() call
1.3 call super(n) for Employee(String)
1.3.1 bind constructor parameters: n="Joe Smith"
1.3.2 call this(n, null) for Employee(String, Date)
1.3.2.1 bind constructor parameters: n="Joe Smith", DoB=null
1.3.2.2 no explicit this() call
1.3.2.3 call super() for Object()
1.3.2.3.1 no binding necessary
1.3.2.3.2 no this() call
1.3.2.3.3 no super() call (Object is the root)
1.3.2.3.4 no explicit variable initialization for Object
1.3.2.3.5 no method body to call
1.3.2.4 initialize explicit Employee variables: salary=15000.00;注意:在父構造器返回後子類才會初始化例項變數的值。
1.3.2.5 execute body: name="Joe Smith"; date=null;
1.3.3 - 1.3.4 steps skipped
1.3.5 execute body: no body in Employee(String)
1.4 no explicit initializers for Manager
1.5 execute body: department="Sales"


這個流程就說明了一切,這個步驟是要注意的。一會還有些內容是要涉及到這裡的。




寫在後邊的一些在使用構造器中的注意事項。


一、構造器中一定不要建立自身的例項,否則會造成呼叫棧溢位錯誤。這個規則也適用於物件的例項變數,如果物件中有自身的引用,這個引用一定不能在定義中或者構造器中初始化。


class a
{
a _a = new a();


public a()
{
_a = new a();
a _b = new a();
}
}


以上三種情況都會造成棧溢位,呵呵,這樣會造成一個無窮遞迴的呼叫棧。


二、如果父類是一個抽象類,那通過呼叫父類的構造器,也可以將它初始化,並且初始化其中的資料。
三、如果你要在構造器中呼叫一個方法時,將該方法宣告為private。
對於這個規則是需要一些說明的,假使你的父類構造器中要呼叫一個非靜態方法,而這個方法不是private的又被子類所過載,這樣在實際建立子類的過程中遞迴呼叫到了父類的構造器時,父類構造器對這個方法的呼叫就會由於多型而實際上呼叫了子類的方法,當這個子類方法需要用到子類中例項變數的時候,就會由於變數沒有初始化而出現異常(至於為什麼子類中的例項變數沒有初始化可以參考上邊的例項初始化過程),這是Java不想看到的情況。而當父類構造器中呼叫的方法是一個private方法時,多型就不會出現,也就不會出現父類構造器呼叫子類方法的情況,這樣可以保證父類始終呼叫自己的方法,即使這個方法中呼叫了父類中的例項變數也不會出現變數未初始化的情況(變數初始化總是在當前類構造器主體執行之前進行)。