在我剛剛接觸如今這個產品的時候,我就在咱們的代碼中接觸到了對Double Brace Initialization的使用。那段代碼用來初始化一個集合:html
1 final Set<String> exclusions = new HashSet<String>() {{ 2 add(‘Alice’); 3 add(‘Bob’); 4 add(‘Marine’); 5 }};
相信第一次看到這種使用方式的讀者和我當時的感受同樣:這是在作什麼?固然,經過在函數add()的調用處加上斷點,您就會了解到這其實是在使用add()函數向剛剛建立的集合exclusions中添加元素。ide
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對集合進行初始化就十分簡單明瞭:ui
1 final Set<String> exclusions = new HashSet<String>() {{ 2 add(‘Alice’); 3 add(‘Bob’); 4 add(‘Marine’); 5 }};
所以對於一個熟悉該使用方法的人來講,Double Brace Initialization清晰簡潔,代碼可讀性好維護性高,天然是初始化集合時的不二選擇。而對於一個沒有接觸過該使用方法並且基礎不是很牢靠的人來講,Double Brace Initialization實在是有些晦澀難懂。spa
從晦澀到熟悉實際上很是簡單,那就是了解它的工做原理。若是將上面的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>的匿名類:code
1 final Set<String> exclusions = new HashSet<String>() { 2 // 匿名派生類的各個成員 3 };
而內層的花括號其實是在匿名派生類內部所聲明的instance initializer:htm
1 final Set<String> exclusions = new HashSet<String>() { 2 { 3 // 因爲匿名類中不能添加構造函數,所以這裏的instance initializer 4 // 實際上等於構造函數,用來執行對當前匿名類實例的初始化 5 } 6 };
在經過Double Brace Initialization建立一個集合的時候,咱們所獲得的其實是一個從集合類派生出的匿名類。在該匿名類初始化時,它內部所聲明的instance initializer就會被執行,進而容許其中的函數調用add()來向剛剛建立好的集合添加元素。blog
其實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