一、介紹
初始化是一個語言十分重要的部分,許多C程序的錯誤就來自於編寫者沒有認真將每一個所定義的變量初始化,隨着代碼量的增加,某個變量的沒有初始化往往會帶來十分嚴重的后果,C++中引入的是構造器的概念,並提供了構造函數。Java也采用了構造器,並額外提供了垃圾回收器,對不再使用的內存進行自動回收。
二、 用構造器保證初始化
Java中的構造器有兩個特點:1、構造器的名字與類的名字相同,防止了與類方法名的沖突,構造器是沒有返回值的。2、調用構造器是編譯器的任務,編寫者只需要告訴編譯器調用哪個構造函數,那構造器之間如何區別呢?
這就設計到方法的重載,構造器的名字是相同的,沒有返回值,唯一的區別就是函數的參數不同,每一個構造函數都有一個獨一無二的參數列表,而這個獨一無二有兩個含義:一方面是參數的類型是獨一無二的,另一方面是參數的順序同樣也是獨一無二的,如:
1 class Gou{ 2 String name; 3 Gou(int x,int y){ 4 System.out.println(x+y); 5 } 6 7 Gou(int y ,int x,char t){ 8 System.out.println(x+y+t); 9 } 10 11 Gou(char t ,int x, int y){ 12 System.out.println(x+y+t+2); 13 } 14 } 15 class x{ 16 public static void main(String[] args) { 17 Gou dog = new Gou(1,2); 18 Gou dog1 = new Gou(1,2,'x'); 19 Gou dog2 = new Gou('x',1,2); 20 } 21 22 }
在17、18、19行三種初始化的方式調用了三種不同的構造函數,說明順序也可以影響到構造函數的類型,如果你在類對象中定義了一個變量類型相同和順序也相同的構造函數,編譯器就會報錯,當然這種換順序改變構造器的方法並不是特別好,它會使程序難以去維護。
這種構造器下如何設計到基本數據類型,如int、short等,如果構造器定義的是一個精度較大的數據類型,傳入一個精度較小的數據會發生什么?或者反過來的情況。我們來嘗試一下:
1、當構造器中定義的類型精度大於傳入的數據類型,如:
1 class Gou{ 2 Gou(int x ){ 3 System.out.println("int"); 4 } 5 Gou(Float x){ 6 System.out.println("Float"); 7 } 8 Gou(Double x){ 9 System.out.println("Double"); 10 } 11 Gou(long x){ 12 System.out.println("Long"); 13 } 14 } 15 class x{ 16 public static void main(String[] args) { 17 float x =1; 18 Gou c = new Gou(x); 19 } 20 }
當有合適的數據類型進行匹配時,會優先匹配同樣的,當我們嘗試將此時的float類型的構造函數刪除時,此時程序就會報錯,需要進行強制轉換將它的精度擴大成double,而對於char、shot等低於int型的變量而言,情況似乎又變化了,如果傳入一個char類型的變量,而次數構造器最小的精度為int,此時編譯器會自動將char擴展成int,使用int的構造器進行對象的初始化,這一點有一點特殊。
2、當傳入的參數大於構造器定義參數的精度
1 class Gou{ 2 Gou(int x ){ 3 System.out.println("int"); 4 } 5 6 Gou(long x){ 7 System.out.println("Long"); 8 } 9 } 10 class x{ 11 public static void main(String[] args) { 12 float x =1; 13 double xt = 1 14 Gou c = new Gou((int)x); 15 Gou d = new Gou((double)x);//錯誤 16 Gou e = new Gou((int)xt); 17 } 18 }
當我們傳入一個float類型的值時,我們可以利用強制類型轉換去使用低精度的構造器,如果你介意數據可能會出現錯誤,盡量不要用這種方法。而對於float和double的轉換編譯器依舊會報錯。
那如果我們沒有寫構造器呢?編譯器會自動幫你創建一個默認的構造器,這個構造器沒有參數,同時函數體也沒有任何東西,但如果你已經創建了自己的構造器,編譯器就不會創建這樣一個無參構造器,如果你創建了有參構造器,仍然想無參數的去創建一個對象,就需要自己手動添加一個無參數的構造器。
三、this關鍵字
你是否思考過這樣一個情況:
1 class Gou{ 2 public void eat(int x){ 3 4 } 5 } 6 class x{ 7 public static void main(String[] args) { 8 Gou dog = new Gou(); 9 Gou dog1 = new Gou(); 10 dog.eat(1); 11 dog1.eat(1); 12 } 13 }
當我們用不同對象調用同一個方法的時候,我們傳的都是同一個參數,編譯器是如何知道我們用的是不同的對象,其實這里編譯器暗自把不同對象的一個引用當作參數傳入了類對象:
1 dog.eat(dog,1); 2 dog1.eat(dog1,1);
真實發生的故事應該是這樣,但這是內部發生的事情,我們不能這樣書寫,如果你想在類的內部截獲這樣一個偷偷傳入進來的引用變量,就需要我將要介紹的this關鍵字,this引用變量並沒有和其他引用變量有什么區別,如:
1 class Gou{ 2 int x; 3 public Gou eat(int x){ 4 this.x = x; 5 return this;//返回這個引用變量 6 } 7 } 8 class x{ 9 public static void main(String[] args) { 10 Gou dog = new Gou().eat(10); 11 } 12 }
這段代碼里發生了一個很有意思的事情,我們用默認構造器創建一個對象后,我們直接調用了這個創建對象的eat方法,這個eat方法返回的是一個Gou類型的對象,所以我們直接返回this,注意這個返回的dog接受的對象是我們一開始所new的那個對象嘛?如果你看了上文,現在肯定有了答案,是的,我們在調用eat方法時會傳入一個此時對象的引用變量,而這個對象就是我們才用new創建,所以就出現我們將對象無意識的傳入再返回出來用我們的dog接受的情況。當然我們傳入的引用變量肯定是沒有了,我們重新用dog做了這個對象的引用變量。
當我們需要在構造器里面調用其他構造器的時候,this也可以起作用,如:
1 class Gou{ 2 int x; 3 Gou(int x){ 4 this(x,x); 5 } 6 Gou(int p,int x){ 7 } 8 }
在這段代碼中我們在一個構造器中調用了另一個構造器,因為是在類內部,所以不需要new一個對象,直接this(參數)即可調用合適的構造器,而這樣調用也不是隨意的,如果你需要在構造器中調用另一個構造器,就需要把這句話寫在第一行,並且你只能調用一次其他的構造器。this其實就是一個對象本身的屬性,它存在於對象當中,等着合適的時候給對象去使用。
四、成員的初始化
Java創建構造器的目的就是來初始化對象,而調用了構造器,成員內部變量又是如何初始化的呢?
在Java中編譯器盡量保證所有成員的初始化,對於類變量(與C語言里面的全局變量類似)來說,編譯器會自動給它賦一個初值。對於局部變量,也就是類方法里定義的變量,就需要編寫者給出一個初值,不然編譯會報錯。
那對於類變量,我們去賦初值的方法有很多,最直接的就是直接在定義的時候去賦予它初值:
1 class Gou{ 2 int x = 0; 3 int j = x; 4 String t = ""; 5 float q = 0; 6 } 7 class s{ 8 Gou x = new Gou(); 9 }
可以看到,無論是基本數據類型,還是自定義的類都可以在定義的時候直接給一個初值,當然也可以另用方法來對類變量進行初始化,如:
1 class Gou{ 2 int x = f(); 3 int t = g(x); 4 int p = h(n);//錯誤 5 int f(){ 6 return 1; 7 } 8 int g(int x){ 9 return x; 10 } 11 12 int h(int x ) { 13 return x; 14 } 15 16 }
可以看到可以用無參方法去初始化,也可以用有參數的,而這個參數必須已經被初始化,不然編譯器會報錯。那如果你執行了你的初始化,編譯器的初始化會不執行嗎?答案是不會,你無法阻止自動初始化的進行。在上述代碼中對於x的初始化,首先定義x的時候編譯器會自動給一個初值,這里是int給的是0(數據類似默認的初值),給了這個初值后,才會執行你定義的初始化動作,將x賦值為0。所以對於類成員而言,編譯器似乎不在意你是否給出了初始化的動作,因為初始化早已得到保證。
那初始化的順序是怎么樣的呢?在類的內部,變量定義的先后順序決定了初始化的順序,即使變量的定義散落在各個方法定義之間,它們仍然會在各個類方法被調用之前被初始化。這里也有一個特殊的情況,萬一成員變量是Static類型的呢?Static型的變量其實在類加載的時候就已經初始化完成,而這種初始化是編譯器默認的,當然如果你想自己初始化,基本方法與普通變量沒有什么區別。說了比較多的情況我們總結一下一個類是如何初始化的,假如有一個叫Dog的類:
- 如果沒有顯式的使用Static關鍵字,構造器實際上就是一個Static類型的方法,因此,當首次創建類型為Dog的對象的時候,或者調用Dog里的Static方法的時候,此時java的解釋器必須查找這個類的位置,也就是在內存里的路徑,以定位Dog.class文件。
- 然后載入Dog.class(其實創建了一個Class對象),有關靜態初始化的動作都會執行,因此,靜態初始化只會出現在Class對象首先加載的時候(也就是這個類加載的時候),並且只會進行這一次。
- 當用new Dog()時,也就是真正創建一個類的對象的時候,首先會在堆上為Dog的對象分配足夠的空間。
- 然后這塊存儲空間會被清0,清0的意思就是自動將Dog對象里的所有基本數據類型都設置成默認的初值(如int型為0 等等)。
- 然后再會執行字段定義處所有初始化的動作(也就是編寫者希望得到的初值)。
- 執行類的構造器,至此一個類對象所有的初始化動作全部完成。
五、finalize方法
談完了對象創建和初始化的過程,我想簡單談一下一個對象的銷毀,Java性能強大的原因與其垃圾回收機制密不可分,編寫者用完一個對象后,可以丟給回收機制去處理,並不會發生忘記delete一個變量或者對象而產生內存泄漏的情況,當然這也不是一直有效,當你的對象獲得一塊特殊的內存地址,這個地址不是由new過來的,在這種情況垃圾回收就失效了,所以java提供一個finalize方法,一旦回收器准備來回收對象了,就先調用這個finalize方法,讓編寫者先把回收器處理不了的垃圾處理掉,再進行正常的回收。
Java垃圾回收器有自己的機制,這里介紹三點:
- 對象可能不被垃圾回收
- 垃圾回收不等於C++里面的析構函數
- 垃圾回收只與內存相關
對於第一點對象可能不被垃圾回收的意思就是如果程序員將對象丟棄后,垃圾回收器不是一定會將其立馬回收,它有自己的回收機制,當內存足夠的時候,它認為不用浪費時間去回收你丟棄的這個對象,所以這就導致你的finalize方法的調用也不是一定發生的,如果垃圾回收器一直沒有回收你這個對象,對象里的finalize方法就永遠不會被調用。這就解釋了與析構函數的區別,你寫了釋放特定區域的方法,但不一定會調用。第三點的意思就是使用垃圾回收的唯一原因就是回收程序不再使用的內存,只要是發生了類似new的情況,垃圾回收都會去釋放它們,而對於使用finalize方法來釋放內存這個主要發生在“本地方法”這個情況當中。
那既然垃圾回收不保證發生,那finalize有其他用途嘛?有!
當一個對象成員不被需要后,需要被清理時,而這個對象可能打開了某個文件,我們要再這個程序消失前去關閉這個文件,而如果沒有執行這個動作造成的錯誤又很難去發現,所以這里就可以用finalize來去保證這個動作一定進行,也就是說在對象被垃圾回收前,一定會執行finalize方法,就很容易發現這個對象所打開的文件是否關閉。