建議36:使用構造程式碼塊精簡程式
什麼叫做程式碼塊(Code Block)?用大括號把多行程式碼封裝在一起,形成一個獨立的資料體,實現特定演算法的程式碼集合即為程式碼塊,一般來說程式碼快不能單獨執行的,必須要有執行主體。在Java中一共有四種型別的程式碼塊:
- 普通程式碼塊:就是在方法後面使用"{}"括起來的程式碼片段,它不能單獨執行,必須通過方法名呼叫執行;
- 靜態程式碼塊:在類中使用static修飾,並用"{}"括起來的程式碼片段,用於靜態變數初始化或物件建立前的環境初始化。
- 同步程式碼塊:使用synchronized關鍵字修飾,並使用"{}"括起來的程式碼片段,它表示同一時間只能有一個執行緒進入到該方法塊中,是一種多執行緒保護機制。
- 構造程式碼塊:在類中沒有任何字首和字尾,並使用"{}"括起來的程式碼片段;
我麼知道一個類中至少有一個建構函式(如果沒有,編譯器會無私的為其建立一個無參建構函式),建構函式是在物件生成時呼叫的,那現在為你來了:建構函式和程式碼塊是什麼關係,構造程式碼塊是在什麼時候執行的?在回答這個問題之前,我們先看看編譯器是如何處理構造程式碼塊的,看如下程式碼:
1 public class Client36 { 2 3 { 4 // 構造程式碼塊 5 System.out.println("執行構造程式碼塊"); 6 } 7 8 public Client36() { 9 System.out.println("執行無參構造"); 10 } 11 12 public Client36(String name) { 13 System.out.println("執行有參構造"); 14 }15 }
這是一段非常簡單的程式碼,它包含了構造程式碼塊、無參構造、有參構造,我們知道程式碼塊不具有獨立執行能力,那麼編譯器是如何處理構造程式碼塊的呢?很簡單,編譯器會把構造程式碼塊插入到每個建構函式的最前端,上面的程式碼等價於:
1 public class Client36 { 2 3 public Client36() { 4 System.out.println("執行構造程式碼塊"); 5 System.out.println("執行無參構造"); 6 } 7 8 public Client36(String name) { 9 System.out.println("執行構造程式碼塊"); 10 System.out.println("執行有參構造"); 11 } 12 }
每個建構函式的最前端都被插入了構造程式碼塊,很顯然,在通過new關鍵字生成一個例項時會先執行構造程式碼塊,然後再執行其他程式碼,也就是說:構造程式碼塊會在每個建構函式內首先執行(需要注意的是:構造程式碼塊不是在建構函式之前執行的,它依託於建構函式的執行),明白了這一點,我們就可以把構造程式碼塊應用到如下場景中:
- 初始化例項變數(Instance Variable):如果每個建構函式都要初始化變數,可以通過構造程式碼塊來實現。當然也可以通過定義一個方法,然後在每個建構函式中呼叫該方法來實現,沒錯,可以解決,但是要在每個建構函式中都呼叫該方法,而這就是其缺點,若採用構造程式碼塊的方式則不用定義和呼叫,會直接由編譯器寫入到每個建構函式中,這才是解決此問題的絕佳方式。
- 初始化例項環境:一個物件必須在適當的場景下才能存在,如果沒有適當的場景,則就需要在建立該物件的時候建立次場景,例如在JEE開發中,要產生HTTP Request必須首先建立HTTP Session,在建立HTTP Request時就可以通過構造程式碼塊來檢查HTTP Session是否已經存在,不存在則建立之。
以上兩個場景利用了構造程式碼塊的兩個特性:在每個建構函式中都執行和在建構函式中它會首先執行。很好的利用構造程式碼塊的這連個特性不僅可以減少程式碼量,還可以讓程式更容易閱讀,特別是當所有的建構函式都要實現邏輯,而且這部分邏輯有很複雜時,這時就可以通過編寫多個構造程式碼塊來實現。每個程式碼塊完成不同的業務邏輯(當然了建構函式儘量簡單,這是基本原則),按照業務順序一次存放,這樣在建立例項物件時JVM就會按照順序依次執行,實現複雜物件的模組化建立。
建議37:構造程式碼塊會想你所想
上一建議中我們提議使用構造程式碼塊來簡化程式碼,並且也瞭解到編譯器會自動把構造程式碼塊插入到各個建構函式中,那我們接下來看看,編譯器是不是足夠聰明,能為我們解決真實的開發問題,有這樣一個案例,統計一個類的例項變數數。你可要說了,這很簡單,在每個建構函式中加入一個物件計數器補救解決了嘛?或者我們使用上一建議介紹的,使用構造程式碼塊也可以,確實如此,我們來看如下程式碼是否可行:
1 public class Client37 { 2 public static void main(String[] args) { 3 new Student(); 4 new Student("張三"); 5 new Student(10); 6 System.out.println("例項物件數量:"+Student.getNumOfObjects()); 7 } 8 } 9 10 class Student { 11 // 物件計數器 12 private static int numOfObjects = 0; 13 14 { 15 // 構造程式碼塊,計算產生的物件數量 16 numOfObjects++; 17 } 18 19 public Student() { 20 21 } 22 23 // 有參構造呼叫無參構造 24 public Student(String stuName) { 25 this(); 26 } 27 28 // 有參構造不呼叫無參構造 29 public Student(int stuAge) { 30 31 } 32 //返回在一個JVM中,建立了多少例項物件 33 public static int getNumOfObjects(){ 34 return numOfObjects; 35 } 36 }
這段程式碼可行嗎?能計算出例項物件的數量嗎?如果編譯器把構造程式碼塊插入到各個建構函式中,那帶有String形參的建構函式就可能有問題,它會呼叫無參構造,那通過它生成的Student物件就會執行兩次構造程式碼塊:一次是無參建構函式呼叫構造程式碼塊,一次是執行自身的構造程式碼塊,這樣的話計算就不準確了,main函式實際在記憶體中產生了3個物件,但結果確是4。不過真的是這樣嗎?我們執行之後,結果是:
例項物件數量:3;
例項物件的數量還是3,程式沒有問題,奇怪嗎?不奇怪,上一建議是說編譯器會把構造程式碼塊插入到每一個建構函式中,但是有一個例外的情況沒有說明:如果遇到this關鍵字(也就是建構函式呼叫自身的其它建構函式時),則不插入構造程式碼塊,對於我們的例子來說,編譯器在編譯時發現String形參的建構函式呼叫了無參構造,於是放棄插入構造程式碼塊,所以只執行了一次構造程式碼塊。
那Java編譯器為何如此聰明?這還要從構造程式碼塊的誕生說起,構造程式碼塊是為了提取建構函式的共同量,減少各個建構函式的程式碼產生的,因此,Java就很聰明的認為把程式碼插入到this方法的建構函式中即可,而呼叫其它的建構函式則不插入,確保每個建構函式只執行一次構造程式碼塊。
還有一點需要說明,大家千萬不要以為this是特殊情況,那super也會類似處理了,其實不會,在構造塊的處理上,super方法沒有任何特殊的地方,編譯器只把構造程式碼塊插入到super方法之後執行而已。僅此不同。
注意:放心的使用構造程式碼塊吧,Java已經想你所想了。
建議38:使用靜態內部類提高封裝性
Java中的巢狀類(Nested Class)分為兩種:靜態內部類(也叫靜態巢狀類,Static Nested Class)和內部類(Inner Class)。本次主要看看靜態內部類。什麼是靜態內部類呢?是內部類,並且是靜態(static修飾)的即為靜態內部類,只有在是靜態內部類的情況下才能把static修飾符放在類前,其它任何時候static都是不能修飾類的。
靜態內部類的形式很好理解,但是為什麼需要靜態內部類呢?那是因為靜態內部類有兩個優點:加強了類的封裝和提高了程式碼的可讀性,我們通過下面程式碼來解釋這兩個優點。
1 public class Person { 2 // 姓名 3 private String name; 4 // 家庭 5 private Home home; 6 7 public Person(String _name) { 8 name = _name; 9 } 10 11 /* home、name的setter和getter方法略 */ 12 13 public static class Home { 14 // 家庭地址 15 private String address; 16 // 家庭電話 17 private String tel; 18 19 public Home(String _address, String _tel) { 20 address = _address; 21 tel = _tel; 22 } 23 /* address、tel的setter和getter方法略 */ 24 } 25 }
其中,Person類中定義了一個靜態內部類Home,它表示的意思是"人的家庭資訊",由於Home類封裝了家庭資訊,不用再Person中再定義homeAddr,homeTel等屬性,這就使封裝性提高了。同時我們僅僅通過程式碼就可以分析出Person和Home之間的強關聯關係,也就是說語義增強了,可讀性提高了。所以在使用時就會非常清楚它表達的含義。
public static void main(String[] args) { // 定義張三這個人 Person p = new Person("張三"); // 設定張三的家庭資訊 p.setHome(new Home("北京", "010")); }
定義張三這個人,然後通過Person.Home類設定張三的家庭資訊,這是不是就和我們真是世界的情形相同了?先登記人的主要資訊,然後登記人員的分類資訊。可能你由要問了,這和我們一般定義的類有神麼區別呢?又有什麼吸引人的地方呢?如下所示:
- 提高封裝性:從程式碼的位置上來講,靜態內部類放置在外部類內,其程式碼層意義就是,靜態內部類是外部類的子行為或子屬性,兩者之間保持著一定的關係,比如在我們的例子中,看到Home類就知道它是Person的home資訊。
- 提高程式碼的可讀性:相關聯的程式碼放在一起,可讀性肯定提高了。
- 形似內部,神似外部:靜態內部類雖然存在於外部類內,而且編譯後的類檔案也包含外部類(格式是:外部類+$+內部類),但是它可以脫離外部類存在,也就說我們仍然可以通過new Home()宣告一個home物件,只是需要匯入"Person.Home"而已。
解釋了這麼多,大家可能會覺得外部類和靜態內部類之間是組合關係(Composition)了,這是錯誤的,外部類和靜態內部類之間有強關聯關係,這僅僅表現在"字面上",而深層次的抽象意義則依類的設計.
那靜態類內部類和普通內部類有什麼區別呢?下面就來說明一下:
- 靜態內部類不持有外部類的引用:在普通內部類中,我們可以直接訪問外部類的屬性、方法,即使是private型別也可以訪問,這是因為內部類持有一個外部類的引用,可以自由訪問。而靜態內部類,則只可以訪問外部類的靜態方法和靜態屬性(如果是private許可權也能訪問,這是由其程式碼位置決定的),其它的則不能訪問。
- 靜態內部類不依賴外部類:普通內部類與外部類之間是相互依賴關係,內部類例項不能脫離外部類例項,也就是說它們會同生共死,一起宣告,一起被垃圾回收,而靜態內部類是可以獨立存在的,即使外部類消亡了,靜態內部類也是可以存在的。
- 普通內部類不能宣告static的方法和變數:普通內部類不能宣告static的方法和變數,注意這裡說的是變數,常量(也就是final static 修飾的屬性)還是可以的,而靜態內部類形似外部類,沒有任何限制。
建議39:使用匿名類的建構函式
閱讀如下程式碼,看上是否可以編譯:
public static void main(String[] args) { List list1=new ArrayList(); List list2=new ArrayList(){}; List list3=new ArrayList(){{}}; System.out.println(list1.getClass() == list2.getClass()); System.out.println(list2.getClass() == list3.getClass()); System.out.println(list1.getClass() == list3.getClass()); }
注意ArrayList後面的不通點:list1變數後面什麼都沒有,list2後面有一對{},list3後面有兩個巢狀的{},這段程式能否編譯呢?若能編譯,那輸結果是什麼呢?
答案是能編譯,輸出的是3個false。list1很容易理解,就是生命了ArrayList的例項物件,那list2和list3代表的是什麼呢?
(1)、list2 = new ArrayList(){}:list2代表的是一個匿名類的宣告和賦值,它定義了一個繼承於ArrayList的匿名類,只是沒有任何覆寫的方法而已,其程式碼類似於:
// 定義一個繼承ArrayList的內部類 class Sub extends ArrayList { } // 宣告和賦值 List list2 = new Sub();
(2)、list3 = new ArrayList(){{}}:這個語句就有點奇怪了,帶了兩對{},我們分開解釋就明白了,這也是一個匿名類的定義,它的程式碼類似於:
// 定義一個繼承ArrayList的內部類 class Sub extends ArrayList { { //初始化程式碼塊 } } // 宣告和賦值 List list3 = new Sub();
看到了吧,就是多了一個初始化塊而已,起到建構函式的功能,我們知道一個類肯定有一個建構函式,而且建構函式的名稱和類名相同,那問題來了:匿名類的建構函式是什麼呢?它沒有名字呀!很顯然,初始化塊就是它的建構函式。當然,一個類中的建構函式塊可以是多個,也就是說會出現如下程式碼:
List list4 = new ArrayList(){{} {} {} {} {}};
上面的程式碼是正確無誤,沒有任何問題的,現在清楚了,匿名類雖然沒有名字,但也是可以有建構函式的,它用建構函式塊來代替建構函式,那上面的3個輸出就很明顯了:雖然父類相同,但是類還是不同的。
建議40:匿名類的建構函式很特殊
在上一建議中我們講到匿名類雖然沒有名字,但可以有一個初始化塊來充當建構函式,那這個建構函式是否就和普通的建構函式完全不一樣呢?我們來看一個例子,設計一個計算器,進行加減運算,程式碼如下:
1 public class Calculator { 2 enum Ops { 3 ADD, SUB 4 }; 5 6 private int i, j, result; 7 8 // 無參構造 9 public Calculator() { 10 11 } 12 13 // 有參構造 14 public Calculator(int _i, int _j) { 15 i = _i; 16 j = _j; 17 } 18 19 // 設定符號,是加法運算還是減法運算 20 protected void setOperator(Ops _ops) { 21 result = _ops.equals(Ops.ADD) ? i + j : i - j; 22 } 23 24 // 取得運算結果 25 public int getResult() { 26 return result; 27 } 28 29 }
程式碼的意圖是,通過建構函式傳遞兩個int型別的數字,然後根據設定的操作符(加法還是減法)進行運算,編寫一個客戶端呼叫:
public static void main(String[] args) { Calculator c1 = new Calculator(1, 2) { { setOperator(Ops.ADD); } }; System.out.println(c1.getResult()); }
這段匿名類的程式碼非常清晰:接收兩個引數1和2,然後設定一個操作符號,計算其值,結果是3,這毫無疑問,但是這中間隱藏著一個問題:帶有引數的匿名類宣告時到底呼叫的是哪一個建構函式呢?我們把這段程式模擬一下:
//加法計算 class Add extends Calculator{ { setOperator(Ops.ADD); } //覆寫父類的構造方法 public Add(int _i, int _j){ } }
匿名類和這個Add類等價嗎?可能有人會說:上面只是把匿名類增加了一個名字,其它的都沒有改動,那肯定是等價了,毫無疑問 ,那好,編寫一個客戶端呼叫Add類的方法看看。程式碼就略了,因為很簡單new Add,然後呼叫父類的getResult方法就可以了,經過測試,輸出結果為0(為什麼而是0?這很容易,有參構造沒有賦值)。這說明兩者不等價,不過,原因何在呢?
因為匿名類的建構函式特殊處理機制,一般類(也就是沒有顯示名字的類)的所有建構函式預設都是呼叫父類的無參建構函式的,而匿名類因為沒有名字,只能由構造程式碼塊代替,也就無所謂有參和無參的建構函式了,它在初始化時直接呼叫了父類的同引數建構函式,然後再呼叫了自己的構造程式碼塊,也就是說上面的匿名類和下面的程式碼是等價的:
//加法計算 class Add extends Calculator{ { setOperator(Ops.ADD); } //覆寫父類的構造方法 public Add(int _i, int _j){ super(_i,_j); } }
它會首先呼叫父類有兩個引數的建構函式,而不是無參構造,這是匿名類的建構函式與普通類的差別,但是這一點也確實鮮有人仔細琢磨,因為它的處理機制符合習慣呀,我傳遞兩個引數,就是希望先呼叫父類有兩個引數的構造,然後再執行我自己的建構函式,而Java的處理機制也正是如此處理的。