千萬不要再這樣創建集合了!極容易記憶體泄露!
- 2021 年 9 月 27 日
- 筆記
GitHub 21.5k Star 的Java工程師成神之路,不來了解一下嗎!
GitHub 21.5k Star 的Java工程師成神之路,真的不來了解一下嗎!
由於Java語言的集合框架中(collections, 如list, map, set等)沒有提供任何簡便的語法結構,這使得在建立常量集合時的工作非常繁索。每次建立時我們都要做:
1、定義一個空的集合類變數
2、向這個結合類中逐一添加元素
3、將集合做為參數傳遞給方法
例如,要將一個Set變數傳給一個方法:
Set users = new HashSet();
users.add("Hollis");
users.add("hollis");
users.add("HollisChuang");
users.add("hollis666");
transferUsers(users);
這樣的寫法稍微有些複雜,有沒有簡潔的方式呢?
雙括弧語法初始化集合
其實有一個比較簡潔的方式,那就是雙括弧語法(double-brace syntax
)建立並初始化一個新的集合:
public class DoubleBraceTest {
public static void main(String[] args) {
Set users = new HashSet() {{
add("Hollis");
add("hollis");
add("HollisChuang");
add("hollis666");
}};
}
}
同理,創建並初始化一個HashMap的語法如下:
Map<String,String> users = new HashMap<>() {{
put("Hollis","Hollis");
put("hollis","hollis");
put("HollisChuang","HollisChuang");
}};
不只是Set、Map,jdk中的集合類都可以用這種方式創建並初始化。
當我們使用這種雙括弧語法初始化集合類的時候,在對Java文件進行編譯時,可以發現一個奇怪的現象,使用javac對DoubleBraceTest進行編譯:
javac DoubleBraceTest.java
我們會發現,得到兩個class文件:
DoubleBraceTest.class
DoubleBraceTest$1.class
有經驗的朋友可能一看到這兩個文件就會知道,這裡面一定用到了匿名內部類。
沒錯,使用這個雙括弧初始化的效果是創建匿名內部類。創建的類有一個隱式的this指針指向外部類。
不建議使用這種形式
首先,使用這種形式創建並初始化集合會導致很多內部類被創建。因為每次使用雙大括弧初始化時,都會生成一個新類。如這個例子:
Map hollis = new HashMap(){{
put("firstName", "Hollis");
put("lastName", "Chuang");
put("contacts", new HashMap(){{
put("0", new HashMap(){{
put("blogs", "//www.hollischuang.com");
}});
put("1", new HashMap(){{
put("wechat", "hollischuang");
}});
}});
}};
這會使得很多內部類被創建出來:
DoubleBraceTest$1$1$1.class
DoubleBraceTest$1$1$2.class
DoubleBraceTest$1$1.class
DoubleBraceTest$1.class
DoubleBraceTest.class
這些內部類被創建出來,是需要被類載入器載入的,這就帶來了一些額外的開銷。
如果您使用上面的程式碼在一個方法中創建並初始化一個map,並從方法返回該map,那麼該方法的調用者可能會毫不知情地持有一個無法進行垃圾收集的資源。
public Map getMap() {
Map hollis = new HashMap(){{
put("firstName", "Hollis");
put("lastName", "Chuang");
put("contacts", new HashMap(){{
put("0", new HashMap(){{
put("blogs", "//www.hollischuang.com");
}});
put("1", new HashMap(){{
put("wechat", "hollischuang");
}});
}});
}};
return hollis;
}
我們嘗試通過調用getMap得到這樣一個通過雙括弧初始化出來的map
public class DoubleBraceTest {
public static void main(String[] args) {
DoubleBraceTest doubleBraceTest = new DoubleBraceTest();
Map map = doubleBraceTest.getMap();
}
}
返回的Map現在將包含一個對DoubleBraceTest的實例的引用。讀者可以嘗試這通過debug或者以下方式確認這一事實。
Field field = map.getClass().getDeclaredField("this$0");
field.setAccessible(true);
System.out.println(field.get(map).getClass());
替代方案
很多人使用雙括弧初始化集合,主要是因為他比較方便,可以在定義集合的同時對他進行初始化。
但其實,目前已經有很多方案可以做這個事情了,不需要再使用這種存在風險的方案。
使用Arrays工具類
當我們想要初始化一個List的時候,可以藉助Arrays類,Arrays中提供了asList可以把一個數組轉換成List:
List<String> list2 = Arrays.asList("hollis ", "Hollis", "HollisChuang");
但是需要注意的是,asList 得到的只是一個 Arrays 的內部類,是一個原來數組的視圖 List,因此如果對它進行增刪操作會報錯。
使用Stream
Stream是Java中提供的新特性,他可以對傳入流內部的元素進行篩選、排序、聚合等中間操作(intermediate operate),最後由最終操作(terminal operation)得到前面處理的結果。
我們可以藉助Stream來初始化集合:
List<String> list1 = Stream.of("hollis", "Hollis", "HollisChuang").collect(Collectors.toList());
使用第三方工具類
很多第三方的集合工具類可以實現這個功能,如Guava等:
ImmutableMap.of("k1", "v1", "k2", "v2");
ImmutableList.of("a", "b", "c", "d");
關於Guava和其中定義的不可變集合,我們在後面會詳細介紹
Java 9內置方法
其實在Java 9 中,在List、Map等集合類中已經內置了初始化的方法,如List中包含了12個重載的of方法,就是來做這個事情的:
/**
* Returns an unmodifiable list containing zero elements.
*
* See <a href="#unmodifiable">Unmodifiable Lists</a> for details.
*
* @param <E> the {@code List}'s element type
* @return an empty {@code List}
*
* @since 9
*/
static <E> List<E> of() {
return ImmutableCollections.emptyList();
}
static <E> List<E> of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
static <E> List<E> of(E... elements) {
switch (elements.length) { // implicit null check of elements
case 0:
return ImmutableCollections.emptyList();
case 1:
return new ImmutableCollections.List12<>(elements[0]);
case 2:
return new ImmutableCollections.List12<>(elements[0], elements[1]);
default:
return new ImmutableCollections.ListN<>(elements);
}
}