在我剛剛接觸現在這個產品的時候,我就在我們的代碼中接觸到了對Double Brace Initialization的使用。那段代碼用來初始化一個集合:
1 final Set<String> exclusions = new HashSet<String>() {{ 2 add(‘Alice’); 3 add(‘Bob’); 4 add(‘Marine’); 5 }};
相信第一次看到這種使用方式的讀者和我當時的感覺一樣:這是在做什么?當然,通過在函數add()的調用處加上斷點,您就會了解到這實際上是在使用add()函數向剛剛創建的集合exclusions中添加元素。
Double Brace Initialization簡介
可為什么我們要用這種方式來初始化集合呢?作為比較,我們先來看看通常情況下我們所編寫的具有相同內容集合的初始化代碼:
1 final Set<String> exclusions = new HashSet<String>(); 2 exclusions.add(‘Alice’); 3 exclusions.add(‘Bob’); 4 exclusions.add(‘Marine’);
這些代碼很繁冗,不是么?在編寫這些代碼的時候,我們需要重復鍵入很多次exclusions。同時,這些代碼在軟件開發人員需要檢查到底向該集合中添加了哪些元素的時候也非常惱人。反過來,使用Double Brace Initialization對集合進行初始化就十分簡單明了:
1 final Set<String> exclusions = new HashSet<String>() {{ 2 add(‘Alice’); 3 add(‘Bob’); 4 add(‘Marine’); 5 }};
因此對於一個熟悉該使用方法的人來說,Double Brace Initialization清晰簡潔,代碼可讀性好維護性高,自然是初始化集合時的不二選擇。而對於一個沒有接觸過該使用方法而且基礎不是很牢靠的人來說,Double Brace Initialization實在是有些晦澀難懂。
從晦澀到熟悉實際上非常簡單,那就是了解它的工作原理。如果將上面的Double Brace Initialization示例稍微更改一下格式,相信您會看出一些端倪:
1 final Set<String> exclusions = new HashSet<String>() { 2 { 3 add(‘Alice’); 4 add(‘Bob’); 5 add(‘Marine’); 6 } 7 };
現在您能看出來到底Double Brace Initialization是如何運行的了吧?Double Brace Initialization一共包含兩層花括號。外層的花括號實際上表示當前所創建的是一個派生自HashSet<String>的匿名類:
1 final Set<String> exclusions = new HashSet<String>() { 2 // 匿名派生類的各個成員 3 };
而內層的花括號實際上是在匿名派生類內部所聲明的instance initializer:
1 final Set<String> exclusions = new HashSet<String>() { 2 { 3 // 由於匿名類中不能添加構造函數,因此這里的instance initializer 4 // 實際上等於構造函數,用來執行對當前匿名類實例的初始化 5 } 6 };
在通過Double Brace Initialization創建一個集合的時候,我們所得到的實際上是一個從集合類派生出的匿名類。在該匿名類初始化時,它內部所聲明的instance initializer就會被執行,進而允許其中的函數調用add()來向剛剛創建好的集合添加元素。
其實Double Brace Initialization並不僅僅局限於對集合類型的初始化。實際上,任何類型都可以通過它來執行預初始化:
1 NutritionFacts cocaCola = new NutritionFacts() {{ 2 setCalories(100); 3 setSodium(35); 4 setCarbohydrate(27); 5 }};
看到了吧。這和我另一篇文章中所提及的Fluent Interface模式有異曲同工之妙。
Double Brace Initialization的優缺點
下一步,我們就需要了解Double Brace Initialization的優缺點,從而更好地對它進行使用。
Double Brace Initialization的優點非常明顯:對於熟悉該使用方法的人而言,它具有更好的可讀性以及更好的維護性。
但是Double Brace Initialization同樣具有一系列問題。最嚴重的可能就是Double Brace Initialization會導致內存泄露。在使用Double Brace Initialization的時候,我們實際上創建了一個匿名類。匿名類有一個性質,那就是該匿名類實例將擁有一個包含它的類型的引用。如果我們將該匿名類實例通過函數調用等方式傳到該類型之外,那么對該匿名類的保持實際上會導致外層的類型無法被釋放,進而造成內存泄露。
例如在Joshua Bloch版的Builder類實現中(詳見這篇博文),我們可以在build()函數中使用Double Brace Initialization來生成產品實例:
1 public class NutritionFacts { 2 …… 3 4 public static class Builder { 5 …… 6 public NutritionFacts build() { 7 return new NutritionFacts() {{ 8 setServingSize(100); 9 setServings(3); 10 …… 11 }}; 12 } 13 } 14 }
而在用戶通過該Builder創建一個產品實例的時候,他將會使用如下代碼:
1 NutritionFacts facts = new NutritionFacts.Builder.setXXX()….build();
上面的代碼沒有保持任何對NutritionFacts.Builder的引用,因此在執行完這段代碼后,該段程序所實際使用的內存應該僅僅增加了一個NutritionFacts實例,不是么?答案是否定的。由於在build()函數中使用了Double Brace Initialization,因此在新創建的NutritionFacts實例中會包含一個NutritionFacts.Builder類型的引用。
另外一個缺點則是破壞了equals()函數的語義。在為一個類型實現equals()函數的時候,我們可能需要判斷兩個參與比較的類型是否一致:
1 @Override 2 public boolean equals(Object o) { 3 if (o != null && o.getClass().equals(getClass())) { 4 …… 5 } 6 7 return false; 8 }
這種實現有一定的爭議。爭議點主要在於Joshua Bloch在Effective Java的Item 8中說它違反了里氏替換原則。反駁這種觀點的人則主要認為維護equals()函數返回結果正確性的責任需要由派生類來保證。而且從語義上來說,如果兩個類的類型都不一樣,那么它們之間還彼此相等本身就是一件荒謬的事情。因此在某些類庫的實現中,它們都通過檢查類型的方式強行要求參與比較的兩個實例的類型需要是一致的。
而在使用Double Brace Initialization的時候,我們則創建了一個從目標類型派生的匿名類。就以剛剛所展示的build()函數為例:
1 public class NutritionFacts { 2 …… 3 4 public static class Builder { 5 …… 6 public NutritionFacts build() { 7 return new NutritionFacts() {{ 8 setServingSize(100); 9 setServings(3); 10 …… 11 }}; 12 } 13 } 14 }
在build()函數中,我們所創建的實際上是從NutritionFacts派生的匿名類。如果我們在該段代碼之后添加一個斷點,我們就可以從調試功能中看到該段代碼所創建實例的實際類型是NutritionFacts$1。因此,如果NutritionFacts的equals()函數內部實現判斷了參與比較的兩個實例所具有的類型是否一致,那么我們剛剛通過Double Brace Initialization所得到的NutritionFacts$1類型實例將肯定與其它的NutritionFacts實例不相等。
好,既然我們剛剛提到了匿名類在調試器中的表示,那么我們就需要慎重地考慮這個問題。原因很簡單:在較為復雜的Double Brace Initialization的使用中,這些匿名類的表示會非常難以閱讀。就以下面的代碼為例:
1 Map<String, Object> characterInfo = new HashMap<String, Object>() {{ 2 put("firstName", "John"); 3 put("lastName", "Smith"); 4 put("children", new HashSet<HashMap<String, Object>>() {{ 5 add(new HashMap<String, Object>() {{ 6 put("firstName", "Alice"); 7 put("lastName", "Smith"); 8 }}); 9 add(new HashMap<String, Object>() {{ 10 put("firstName", "George"); 11 put("lastName", "Smith"); 12 }}); 13 }}); 14 }};
而在使用調試器進行調試的時候,您會看到以下一系列類型:
Sample.class
Sample$1.class
Sample$1$1.class
Sample$1$1$1.class
Sample$1$1$2.class
在查看這些數據的時候,我們常常無法直接理解這些數據到底代表的是什么。因此軟件開發人員常常需要查看它們的基類到底是什么,並根據調用棧去查找這些數據的初始化邏輯,才能了解這些數據所具有的真正含義。在這種情況下,Double Brace Initialization所提供的不再是較高的維護性,反而變成了維護的負擔。
同時由於Double Brace Initialization需要創建一個目標類型的派生類,因此我們不能在一個由final修飾的類型上使用Double Brace Initialization。
而且值得一提的是,在某些IDE中,Double Brace Initialization的格式實際上顯得非常奇怪。這使得Double Brace Initialization喪失了其最大優勢。
而且在使用Double Brace Initialization之前,我們首先要問自己:我們是否在使用一系列常量來初始化集合?如果是,那么為什么要將數據和應用邏輯混合在一起?如果這兩個問題中的任意一個是否定的,那么就表示我們應該使用獨立的文件來記錄應用所需要的數據,如*.properties文件等,並在應用運行時加載這些數據。
適當地使用Double Brace Initialization
可以說,Double Brace Initialization雖然在表意上具有突出優勢,它的缺點也非常明顯。因此軟件開發人員需要謹慎地對它進行使用。
在前面的介紹中我們已經看到,Double Brace Initialization最大的問題就是在表達復雜數據的時候反而會增加的維護成本,在equals()函數方面不清晰的語義以及潛在的內存泄露。
第一個缺點非常容易避免,那就是在創建一個復雜的數據集合時,我們不再考慮使用Double Brace Initialization,而是將這些數據存儲在一個專門的數據文件中,並在應用運行時加載。
而后兩個缺點則可以通過限制該部分數據的使用范圍來完成。
那在需要初始化復雜數據的時候,我們應該怎么辦?為此業內也提出了一系列解決方案。這些方案不僅可以提高代碼的表意性,還可以避免由於使用Double Brace Initialization所引入的一系列問題。
最常見的一種解決方案就是使用第三方類庫。例如由Apache Commons類庫提供的ArrayUtils.toMap()函數就提供了一種非常清晰的創建Map的實現:
1 Map<Integer, String> map = (Map) ArrayUtils.toMap(new Object[][] { 2 {1, "one"}, 3 {2, "two"}, 4 {3, "three"} 5 });
如果說您不喜歡引入第三方類庫,您也可以通過創建一個工具函數來完成類似的事情:
Map<Integer, String> map = Utils.toMap(new Object[][] { {1, "one"}, {2, "two"}, {3, "three"} }); public Map<Integer, String> toMap(Object[][] mapData) { …… }
轉載請注明原文地址並標明轉載:http://www.cnblogs.com/loveis715/p/4593962.html
商業轉載請事先與我聯系:silverfox715@sina.com