Java:Double Brace Initialization

來源:互聯網
上載者:User

標籤:

  在我剛剛接觸現在這個產品的時候,我就在我們的代碼中接觸到了對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 initializer4         // 實際上等於建構函式,用來執行對當前匿名類執行個體的初始化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 @Override2 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

商業轉載請事先與我聯絡:[email protected]

Java:Double Brace Initialization

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.