Java編程思想第4版學習筆記(三)
第五章 初始化與清理(構造器和垃圾回收機制)
Java有和C++類似的構造函數來為新創建的對象執行初始化及完成一些特殊的操作,有的類數據成員可能會被初始化幾次,它們的初始化次序和次數是根據程序決定的,可以用重載的構造函數以不同的形式對一個對象初始化,重載的構造函數之間可以通過this互相調用。最后,本章講了finalize()函數和簡單的GC機制,也提到了如何創建一個數組。
知識點1
:P76,5.1,定義構造函數
當對象被創建時,構造函數會自動被調用,構造器的函數名和類名相同,無返回值類型(也不是void類型,就是不需要任何類型),可以有任意個參數,在函數體里寫上你想讓該類對象被創建時會發生的事情。創建對象時要給對象符合構造器(構造函數的另一種說法)要求的參數。不需要任何參數的構造器被稱為“無參構造器”或“默認構造器”。
如果你寫的類沒有任何構造函數,Java會自動幫你創建出一個無參構造器,它做的事只是把類內成員初始化為0或那個類型默認的初始值。如果你寫了一個或多個構造器(無論帶不帶參數),系統將不再自動生成一個無參構造器。
一個含有顯式構造器的類的實例被初始化的順序是這樣的:
在構造函數被調用之前,靜態變量首先被初始化為默認值或類內初始值(這一行為被這本書稱為指定初始化,具體做法是在類字段定義時就用字段=對象;的方式初始化字段),然后一般變量首先被初始化為默認值或類內初始值。之后,會執行接下來會提到的“初始化子句”,最后,構造函數如果有初始化類數據成員的語句,則,這些語句依次對數據成員初始化。另外,靜態類數據成員只要被訪問,哪怕沒有實例存在,也會完成初始化。
知識點2
:P77,5.2,方法重載
同一個類里可以有多個同名函數,這些函數的參數類型或參數數量不一樣使使用者可以通過不同的參數組合調用一個名字的參數,這種形式叫方法重載。定義一個重載的函數,只需要通過正常的定義函數的形式,把函數名設定為想要重載的函數名,把參數列表變成不一樣的就行了。
重載之后,如果傳入的實參類型並非任意一個重載函數需要的類型,但經過非窄化轉換仍能匹配所有的重載函數需要的參數類型,這個參數就會類型提升為更接近它的那一個函數需要的參數類型,參與運算。
知識點3
:P84,5.4,this
this可以出現在非靜態函數體內,代表當前函數所屬對象的一個引用。在一個構造器中調用另一個構造器,必須使用this(構造器參數);的形式,一個構造器中只能通過這種方法調用一次別的構造器,且這個調用要放在函數最開始。
知識點4
:P87,5.5,垃圾回收與finalize()函數
垃圾回收器要回收對象的時候,首先要調用這個類的finalize方法),一般的純Java編寫的Class不需要重新覆蓋這個方法,因為Object已經實現了一個默認的,除非我們要實現特殊的功能。如果想覆蓋Object的finalize方法,只需聲明一個 protected void finalize( ) { } 這樣的函數。在函數里寫一些你想要Java的GC回收對象之前你想做的事情。
Java中有一個有趣的函數,叫System.gc();,這個函數的作用是提醒JVM:程序員覺得應該進行一次垃圾回收了。至於到底JVM真的開始了垃圾回收,還是沒有理你的這個意願,認為根本沒必要浪費時間和資源進行垃圾回收,那就是JVM自己的事情了。
Java垃圾回收的機理在於遍歷對象和對象的引用,只要發現某個對象不可達,即在代碼執行的某一過程中無法通過任何引用(或者這個對象目前根本就沒有引用)訪問這個對象,這個對象就失去的存在的意義,會被回收。不過具體的回收時機,不同的JVM實現上也不盡相同。
知識點5
:P87,5.7.3,靜態初始化子句和實例初始化子句
如果你需要在構造函數里額外執行一些語句,又實在懶得寫構造函數,你可以使用如下所示的方法(靜態/實例初始化子句)實現這個目的。紅色的部分就是初始化子句。初始化子句會在默認初始化和構造器初始化之間被調用。
class staticClass{
static int i = 0;
static{
i = 9;
System.out.println("i = 9");
}
}
class sampleClass{
int i =0;
{
i = 89;
System.out.println("i = 89");
}
static int i = 0;
static{
i = 9;
System.out.println("i = 9");
}
}
class sampleClass{
int i =0;
{
i = 89;
System.out.println("i = 89");
}
}
知識點6
:P98,5.8,數組
數組是一種能夠容納多個元素的重要結構。要定義一個某種類型的數組,只需這樣做:類型名[] 數組名; 或者 類型名 數組名[];
,比如 int[] a1;
和一些語言不同,不能夠用類似C/C++那樣的方式指定數組的大小,只能創建一個數組的引用(即數組名)來操控它代表的真正數組對象。使用一個空(值為null)的數組引用也將出現錯誤,因此要在這個數組引用初始化之后再使用。可以使用初始化表達式,即一對大括號包圍起來的一個元素的列表來初始化數組,比如int[] a = { 1,2,3,4 };,這種初始化方法只能夠用於數組定義之處。也可以把另一個數組的引用所指代的對象傳遞給這個引用,比如int[] a2 = a;。最后,還可以通過new關鍵字創建數組對象和其引用,比如int[] c = new int[30]; 或者 int[] a = new int[]{1,2,3,4}; 來初始化。
正如上一章所說,數組可被用於foreach循環。
數組對象有一個叫做toString的方法,這個方法不需要參數,可以把數組轉換成合適的字符串。也可以使用Array.toString(數組名)的靜態方法達到同樣的目的。
知識點7
:P102,5.8.1,可變參數列表
我們有時候可能需要這么一種函數,這個函數需要傳入一些參數,這些參數類型是固定的,但我們並不知道會傳入幾個這個類型的參數。這時我們就需要這個特性。我們可以在參數列表里放一個數組來解決這個問題。
比如 void func(int 1, char[] c); 我們想要使用這個函數,就可以這么調用它:
func(1, new char[]{'a','b','c'}); 不過這種寫法需要顯式創建一個數組對象並且傳入,我們可以用這種寫法代替原來的函數定義void func(int i, char... c){}; ,這種定義方式可以允許我們這么調用它:func(1,'a','b','c'); ,大大簡略了語法。Java的函數重載時,即使實參可以轉化為多種函數需要的形參類型,不過Java有粒度很細的優先轉換的規則,可以使這個調用總能匹配到一個“最容易轉化”的,因此不會產生二義性調用(C++函數匹配則只有5個等級,每個等級間的各個調用形式是優先度一致的,因此可能產生二義性調用)。
可變參數列表必須放在參數列表的最后且每個函數只能有一個可變形參。
知識點8
:P105,5.9,枚舉類型
和其他主流語言一樣,Java也提供了枚舉類型,它的關鍵字是enum,用來方便的定義枚舉類型,定義枚舉類型的方式類似與定義一個類:enum testSize{ SMALL, MIDIUM, LARGE }; ,使用時,可以為testSize枚舉類型創建一個實例:testSize s = testSize.SMALL; 枚舉類型的對象有toString,ordinal等方法,ordinal()返回一個從0開始的當前枚舉值在枚舉類型中被定義時的次序。此外,枚舉類型還提供values()靜態函數,返回所有枚舉情況的集合,使編程者可以遍歷每個枚舉值。
第五章 練習題
練習1、2:
創建一個類,它包含一個未初始化的String域,一個在定義時就初始化的String域,一個構造器初始化的String域。驗證第一個String域被默認初始化為了null,探究第二和第三個String域的差異。

1 class StringTest{ 2 StringTest(){ 3 s3 = "4567"; 4 } 5 6 String s1; 7 String s2 = "1234"; 8 String s3; 9 } 10 11 public class MainTest { 12 public static void main(String[] args) { 13 StringTest st = new StringTest(); 14 System.out.println(st.s1); 15 System.out.println(st.s2); 16 System.out.println(st.s3); 17 } 18 }
練習3、4:創建一個帶無參構造器的類,在構造器中打印一條信息,為這個類創建一個對象。在為這個類創建一個重載構造器,這個構造器需要一個字符串做參數,並把接收的字符串也打印出來。

1 class Test{ 2 Test(){ 3 System.out.println("Test"); 4 } 5 6 Test(String s){ 7 System.out.println(s+"test"); 8 } 9 } 10 11 public class MainTest { 12 public static void main(String[] args) { 13 Test t = new Test(); 14 Test t2 = new Test("1234"); 15 } 16 }
練習5、6:創建一個名為Dog的類,它具有重載的bark()方法,此方法根據不同的基本數據類型進行重載,並根據被調用的版本,打印出不同類型的barking,howling等信息。編寫對應的主函數調用所有不同版本的方法。再試着寫兩個構造函數,它們都需要兩個不同類型的參數,但是這兩個構造函數需要的參數類型順序正好相反,試試調用它們。

1 class Dog{ 2 Dog(int i){ 3 System.out.println("barking"); 4 } 5 6 Dog(double d){ 7 System.out.println("howling"); 8 } 9 10 Dog(char c){ 11 System.out.println("Meow~"); 12 } 13 14 Dog(int i, boolean b){ 15 System.out.println("I B"); 16 } 17 18 Dog(boolean b, int i){ 19 System.out.println("B I"); 20 } 21 } 22 23 public class MainTest { 24 public static void main(String[] args) { 25 Dog d1 = new Dog(1); 26 Dog d2 = new Dog(1.5); 27 Dog d3 = new Dog('c'); 28 29 Dog bi1 = new Dog(1,true); 30 Dog bi2 = new Dog(true,1); 31 } 32 }
練習7:創建一個沒有構造器的類,並在main()中創建其對象,用以驗證編譯器是否真的自動加入了默認構造器。

1 class Test{ 2 int i; 3 double d; 4 } 5 6 public class MainTest { 7 public static void main(String[] args) { 8 Test t = new Test(); 9 System.out.println(t.i); 10 System.out.println(t.d); 11 } 12 }
練習8:編寫具有兩個方法的類,在第一個方法內調用第二個方法兩次:第一次調用時不使用this關鍵字,第二次調用使用關鍵字,來驗證this關鍵字的作用。

1 class Test{ 2 void method1(){ 3 method2(); 4 this.method2(); 5 } 6 7 void method2(){ System.out.println("Ah,be called!!"); } 8 } 9 10 public class MainTest { 11 public static void main(String[] args) { 12 Test t = new Test(); 13 t.method1(); 14 } 15 }
練習9:編寫具有兩個(重載)構造器的類,並在第一個構造器中通過this調用第二個構造器。

1 class Test{ 2 Test(String s, double i){ 3 this(i); 4 ss = s; 5 6 System.out.println("name: "+ss); 7 System.out.println("Area: "+ii); 8 } 9 10 Test(double i){ 11 ii = i*i*3.14; 12 } 13 14 String ss; 15 double ii; 16 } 17 18 public class MainTest { 19 public static void main(String[] args) { 20 Test t = new Test("Circle",2); 21 } 22 }
練習10、11:編寫具有finalize()方法的類,並在方法中打印消息,在main()中為該類創建一個對象,研究finalize和System.gc()和finalize()的聯系。

1 class Test{ 2 protected void finalize(){ 3 //super.finalize(); 4 System.out.println("Ah!!NO!!!"); 5 } 6 } 7 8 public class MainTest { 9 public static void main(String[] args) { 10 Test t = new Test(); 11 System.gc(); 12 } 13 }
練習12:編寫名為Tank的類,此類的狀態可以是“滿的”或者“空的”,其終結條件是:對象是空的,編寫finalize()函數以在gc之前檢驗對象狀態。

1 class Tank{ 2 protected void finalize(){ 3 //super.finalize(); 4 if(isFull){ 5 System.out.println("Not Good"); 6 } 7 else{ 8 System.out.println("Good!"); 9 } 10 } 11 12 boolean isFull = false; 13 } 14 15 public class MainTest { 16 public static void main(String[] args) { 17 Tank t = new Tank(); 18 System.gc(); 19 } 20 }
練習13:調試書上代碼,略。
練習14:編寫一個類,擁有兩個靜態字符串域,其中一個在定義處初始化,另一個在靜態塊中初始化。現在加入一個靜態方法打印這兩個字段的值,查看它們是否都會在被使用之前完成初始化動作。

1 class Test{ 2 static String s1 = "1234"; 3 static String s2; 4 static{ 5 s2 = "4567"; 6 } 7 8 static void func(){ 9 System.out.println(Test.s1); 10 System.out.println(Test.s2); 11 } 12 } 13 14 public class MainTest { 15 public static void main(String[] args) { 16 Test.func(); 17 } 18 }
練習15:編寫一個含有字符串域的類,並采用實例初始化方式進行初始化。

1 class Test{ 2 String s1; 3 { 4 s1 = new String("1234"); 5 } 6 }
練習16:創建一個String對象數組,並為每一個元素都賦值一個String,用for循環來打印此數組。

1 public class MainTest { 2 public static void main(String[] args) { 3 String[] arrayString = {"1111","2222","3333","4444"}; 4 for(String s:arrayString) 5 { 6 System.out.println(s); 7 } 8 } 9 }
練習17、18:創建一個類,它有一個構造器,這個構造器接收一個String類型的參數。在構造階段,打印此參數。創建一個該類對象的引用數組,但是不實際地創建對象賦值給該數組。試着運行程序。再試着通過創建對象,再賦值給引用數組,從而完成程序。

1 class Test{ 2 Test(String s){ 3 System.out.println(s); 4 } 5 } 6 7 public class MainTest { 8 public static void main(String[] args) { 9 Test[] arrayTest = new Test[]{ 10 new Test("123"), 11 new Test("456") 12 }; 13 } 14 }
練習19:創建一個類,它的構造函數接受一個可變參數的String數組。驗證你可以向該方法傳遞一個用逗號分隔的String實參列表,或是一個String[]。

1 class Test{ 2 Test(String... s){ 3 } 4 } 5 6 public class MainTest { 7 public static void main(String[] args) { 8 Test t1 = new Test("111","222"); 9 Test t2 = new Test(new String[]{"333","444"}); 10 } 11 }
練習20:創建一個使用可變參數列表而不是用普通main()語法的主函數main(),打印args數組的傳入的命令行參數。

1 public class MainTest { 2 public static void main(String... args) { 3 for(String s:args) 4 { 5 System.out.println(s); 6 } 7 } 8 }
練習21、22:創建一個enum,它包含紙幣中最小面值的6種類型,通過values()循環並打印每一個值以及其ordinal()。
最后再試着使用帶enum類型的switch語句。

1 enum CNY{ 2 CNY1,CNY5,CNY10,CNY20,CNY50,CNY100; 3 } 4 5 public class MainTest { 6 public static void main(String[] args) { 7 for (CNY temp : CNY.values()) { 8 System.out.println(temp + " " + temp.ordinal()); 9 } 10 11 CNY cnyTemp = CNY.CNY5; 12 switch (cnyTemp){ 13 case CNY1: 14 System.out.println("1 Yuan"); 15 break; 16 17 case CNY5: 18 System.out.println("5 Yuan"); 19 break; 20 21 case CNY10: 22 System.out.println("10 Yuan"); 23 break; 24 25 case CNY20: 26 System.out.println("20 Yuan"); 27 break; 28 29 case CNY50: 30 System.out.println("50 Yuan"); 31 break; 32 33 case CNY100: 34 System.out.println("100 Yuan"); 35 break; 36 37 default: 38 System.out.println("Error : Fake Money"); 39 } 40 } 41 }