引言
這篇文章我們主要介紹Java初始化和清理的相關內容,這些內容雖然比較基礎,但是還是在這邊做一個簡單的總結,方便以后查閱。
初始化過程
Java盡力保證:所有變量在使用之前都會得到恰當的初始化(對於方法的局部變量,Java會以編譯時錯誤的形式來提醒程序員進行初始化)。
1、類數據成員的初始化
類成員初始化過程是這樣的:當我們實例化一個對象時,編譯器會首先執行數據成員的初始化過程,然后在執行構造器。下面我們通過一個簡單的例子來介紹下類數據成員的初始化過程。代碼如下:
1 class Sample { 2 int value; 3 4 public Sample() { 5 this(0); 6 } 7 8 public Sample(int value) { 9 this.value=value; 10 System.out.println(this.value); 11 } 12 13 @Override 14 public String toString() { 15 return String.valueOf(this.value); 16 } 17 } 18 19 public class PracClass { 20 private int id; 21 private String name; 22 private static Sample sample=new Sample(); 23 24 public PracClass() { 25 this(1,"defaltName"); 26 } 27 28 public PracClass(int id,String name){ 29 this.id=id; 30 this.name=name; 31 System.out.println(this.id+" "+this.name); 32 } 33 34 @Override 35 public String toString() { 36 return this.id+" "+this.name+" SampleValue: "+sample; 37 } 38 39 public static void main(String[] args) { 40 PracClass pracClass=new PracClass(); 41 System.out.println(pracClass); 42 } 43 }
輸出結果如下:
1 0 2 1 defaltName 3 1 defaltName SampleValue: 0
我們看到main函數實例化了一個PracClass對象。在PracClass類中其中有一個static對象sample。我們從輸出結果中,可以看到編譯器先執行了類成員數據的初始化過程(靜態的和非靜態的都是)。然后再執行對象的構造器。
2、繼承關系下的初始化過程
我們來看一下存在繼承關系下,類成員數據是如何初始化的,下面我們還是通過簡單的例子來看一下:
1 class Insect { 2 private int i=9; 3 protected int j; 4 5 public Insect() { 6 System.out.println("i="+i+","+j); 7 j=20; 8 } 9 private static int x1=printInit("static Insect.x1 init"); 10 11 static int printInit(String s){ 12 System.out.println(s); 13 return 16; 14 } 15 } 16 17 18 public class PracClass extends Insect { 19 private int k=printInit("Beetle.k init"); 20 public PracClass() { 21 System.out.println("k="+k); 22 System.out.println("j="+j); 23 } 24 private static int x2=printInit("Beetle.x2 init"); 25 26 public static void main(String[] args) { 27 System.out.println("Beetle contructor"); 28 PracClass pracClass=new PracClass(); 29 } 30 }
我們來看一下運行結果:
1 static Insect.x1 init 2 Beetle.x2 init 3 Beetle contructor 4 i=9,0 5 Beetle.k init 6 k=16 7 j=20
當程序運行時,第一步就是去訪問main方法,這時候加載器開始啟動並且找出PracClass類的編譯代碼么。在這個加載過程中,編譯器注意到它有一個基類,於是它繼續進行加載基類(如果該基類還有其自身的基類,那么第二個基類就會被加載,如此類推)。這時候根基類中的static初始化,然后是下一個導出類。為什么要這樣的?因為導出類的static初始化可能會依賴於基類成員是否被正確初始化。
這時候,必要的類都已經加載完畢。下面開始創建對象,這時候基類的實例變量初始化過程,然后執行基類構造器,然后執行導出類的實例變量初始化過程,然后是導出類的構造器。
清理過程
我們知道Java相比C++最大的一個差異之處在於Java是提供垃圾收集器的。但是垃圾收集器只回收那些使用new關鍵字分配的內存。對於不是使用new關鍵字來獲取的內存,垃圾收集器是不知道如何釋放的。Java為了應對這種情況允許在類中定義一個finalize的方法。它的工作原理是這樣的:一旦垃圾回收器准備好釋放對象占用的存儲空間,將首先調用其finalize方法,並且在下一次垃圾回收時,才會真正的回收對象占用的內存。
通過上面的描述,我們知道finalize方法是不能作為通用的清理方法的。我們建議寫一個顯示的終止方法(類似於Stream中的close、Timer中的cancle等)那么在什么情況下才能使用finalize方法呢?下面兩種情況可以使用finalize方法。
1、finalize方法作為安全網
我們顯示的終結方法一般是使用try-finally結構來使用的。在finally方法內部調用顯示的終止方法,可以保證即使在使用對象過程中發生異常,該終結方法還是會執行。
當對象的所有者忘記調用前面段落中建議的顯示的終止方法時,finalize方法可以充當“安全網”的角色。雖然這樣做不能保證終止方法會被及時的調用。但至少在無法通過調用顯示終止方法來正常結束操作的情況下,遲一點釋放關鍵資源總比不釋放要好。如果我們在終結方法中發現資源還未被終止,那么應該在日志中記錄一條警告,這樣表示顯示的終止方法可能沒有被調用,此處需要進行修復。
2、與本地方法有關
如果在Java中使用JNI技術(即使用C/C++進行內存分配)。那么Java的垃圾收集器是無法進行內存回收的。這時候推薦使用顯示的終止方法來釋放這部分內存(你可以調用C/C++方法釋放)。
簡單總結
finalize方法不推薦使用,如果你想釋放內存/資源,推薦顯示的方法來釋放分配的內存。