生活中的尷尬無處不在,有時候你只是想簡單的裝一把,但某些「老同志」老是在不經意之間,給你無情的一腳,踹得你簡直沒法呼吸。java
但誰讓咱年輕呢?吃虧要趁早,前路會更好。markdown
喝了這口溫熱的雞湯,我們來聊聊是怎麼回事。oop
事情是這樣的,在一個不大不小的項目中,小王寫下了這段代碼:this
Map<String, String> map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); }}; map.forEach((k, v) -> { System.out.println("key:" + k + " value:" + v); }); 複製代碼
原本是用它來替代下面這段代碼的:spa
Map<String, String> map = new HashMap(); map.put("map1", "value1"); map.put("map2", "value2"); map.put("map3", "value3"); map.forEach((k, v) -> { System.out.println("key:" + k + " value:" + v); }); 複製代碼
兩塊代碼的執行結果也是徹底同樣的:設計
key:map3 value:value3調試
key:map2 value:value2code
key:map1 value:value1orm
因此小王正在得意的把這段代碼介紹給部門新來的妹子小甜甜看,卻不巧被正在通過的老張也看到了。對象
老張原本只是想給昨天的枸杞再續上一杯 85° 的熱水,但說來也巧,恰好撞到了一次能在小甜甜面前秀技術的一波機會,因而習慣性的整理了一下本身稀疏的秀髮,便開啓了 diss 模式。
「小王啊,你這個代碼問題很大啊!」
「怎麼能用雙花括號初始化實例呢?」
此時的小王被問的一臉懵逼,心裏有無數個草泥馬奔騰而過,心想你這頭老牛居然也和我爭這顆嫩草,但心裏卻有一種不祥的預感,感受本身要輸,瞬間羞澀的不知該說啥,只能紅着小臉,輕輕的「嗯?」了一聲。
老張:「使用雙花括號初始化實例是會致使內存溢出的啦!儂不曉得嘛?」
小王沉默了片刻,只是憑藉着以往的經驗來看,這「老傢伙」仍是有點東西的,因而敷衍的「哦~」了一聲,彷彿本身明白了怎麼回事同樣,,其實心裏仍然迷茫的一匹,爲了避免讓其餘同事發現,只得這般做態。
因而片刻的敷衍,待老張離去以後,才悄悄的打開了 Google,默默的搜索了一下。
小王:哦,原來如此......
首先,咱們來看使用雙花括號初始化的本質是什麼?
以咱們這段代碼爲例:
Map<String, String> map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); }}; 複製代碼
這段代碼實際上是建立了匿名內部類,而後再進行初始化代碼塊。
這一點咱們可使用命令 javac
將代碼編譯成字節碼以後發現,咱們發現以前的一個類被編譯成兩個字節碼(.class)文件,以下圖所示:
咱們使用 Idea 打開 DoubleBracket$1.class
文件發現:
import java.util.HashMap; class DoubleBracket$1 extends HashMap { DoubleBracket$1(DoubleBracket var1) { this.this$0 = var1; this.put("map1", "value1"); this.put("map2", "value2"); } } 複製代碼
此時咱們能夠確認,它就是一個匿名內部類。那麼問題來了,匿名內部類爲何會致使內存溢出呢?
在 Java 語言中非靜態內部類會持有外部類的引用,從而致使 GC 沒法回收這部分代碼的引用,以致於形成內存溢出。
這個就要從匿名內部類的設計提及了,在 Java 語言中,非靜態匿名內部類的主要做用有兩個。
1、當匿名內部類只在外部類(主類)中使用時,匿名內部類可讓外部不知道它的存在,從而減小了代碼的維護工做。
2、當匿名內部類持有外部類時,它就能夠直接使用外部類中的變量了,這樣能夠很方便的完成調用,以下代碼所示:
public class DoubleBracket { private static String userName = "磊哥"; public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Map<String, String> map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); put(userName, userName); }}; } } 複製代碼
從上述代碼能夠看出在 HashMap
的方法內部,能夠直接使用外部類的變量 userName
。
關於匿名內部類是如何持久外部對象的,咱們能夠經過查看匿名內部類的字節碼得知,咱們使用 javap -c DoubleBracket\$1.class
命令進行查看,其中 $1
爲以匿名類的字節碼,字節碼的內容以下;
javap -c DoubleBracket\$1.class
Compiled from "DoubleBracket.java"
class com.example.DoubleBracket$1 extends java.util.HashMap {
final com.example.DoubleBracket this$0;
com.example.DoubleBracket$1(com.example.DoubleBracket);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/DoubleBracket;
5: aload_0
6: invokespecial #7 // Method java/util/HashMap."<init>":()V
9: aload_0
10: ldc #13 // String map1
12: ldc #15 // String value1
14: invokevirtual #17 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
17: pop
18: aload_0
19: ldc #21 // String map2
21: ldc #23 // String value2
23: invokevirtual #17 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
26: pop
27: return
}
複製代碼
其中,關鍵代碼的在 putfield
這一行,此行表示有一個對 DoubleBracket
的引用被存入到 this$0
中,也就是說這個匿名內部類持有了外部類的引用。
若是您以爲以上字節碼不夠直觀,不要緊,咱們用下面的實際的代碼來證實一下:
import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class DoubleBracket { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { Map map = new DoubleBracket().createMap(); // 獲取一個類的全部字段 Field field = map.getClass().getDeclaredField("this$0"); // 設置容許方法私有的 private 修飾的變量 field.setAccessible(true); System.out.println(field.get(map).getClass()); } public Map createMap() { // 雙花括號初始化 Map map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); }}; return map; } } 複製代碼
當咱們開啓調試模式時,能夠看出 map
中持有了外部對象 DoubleBracket
,以下圖所示:
以上代碼的執行結果爲:
class com.example.DoubleBracket
從以上程序輸出結果能夠看出:匿名內部類持有了外部類的引用,所以咱們纔可使用 $0
正常獲取到外部類,並輸出相關的類信息。
當咱們把如下正常的代碼(無返回值):
public void createMap() { Map map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); }}; // 業務處理.... } 複製代碼
改成下面這個樣子時(返回了 Map 集合),可能會形成內存泄漏:
public Map createMap() { Map map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); }}; return map; } 複製代碼
爲何用了「可能」而不是「必定」會形成內存泄漏?
這是由於當此 map
被賦值爲其餘類屬性時,可能會致使 GC 收集時不清理此對象,這時候纔會致使內存泄漏。能夠關注我「Java中文社羣」後面會專門寫一篇關於此問題的文章。
要想保證雙花扣號不泄漏,辦法也很簡單,只須要將 map
對象聲明爲 static
靜態類型的就能夠了,代碼以下:
public static Map createMap() { Map map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); put("map3", "value3"); }}; return map; } 複製代碼
什麼?你不相信!
不要緊,咱們用事實說話,使用以上代碼,咱們從新編譯一份字節碼,查看匿名類的內容以下:
javap -c DoubleBracket\$1.class
Compiled from "DoubleBracket.java"
class com.example.DoubleBracket$1 extends java.util.HashMap {
com.example.DoubleBracket$1();
Code:
0: aload_0
1: invokespecial #1 // Method java/util/HashMap."<init>":()V
4: aload_0
5: ldc #7 // String map1
7: ldc #9 // String value1
9: invokevirtual #11 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
12: pop
13: aload_0
14: ldc #17 // String map2
16: ldc #19 // String value2
18: invokevirtual #11 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
21: pop
22: aload_0
23: ldc #21 // String map3
25: ldc #23 // String value3
27: invokevirtual #11 // Method put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
30: pop
31: return
}
複製代碼
從此次的代碼咱們能夠看出,已經沒有 putfield
關鍵字這一行了,也就是說靜態匿名類不會持有外部對象的引用了。
緣由其實很簡單,由於匿名內部類是靜態的以後,它所引用的對象或屬性也必須是靜態的了,所以就能夠直接從 JVM 的 Method Area(方法區)獲取到引用而無需持久外部對象了。
即便聲明爲靜態的變量能夠避免內存泄漏,但依舊不建議這樣使用,爲何呢?
緣由很簡單,項目通常都是須要團隊協做的,假如那位老兄在不知情的狀況下把你的 static
給刪掉呢?這就至關於設置了一個隱形的「坑」,其餘不知道的人,一不當心就跳進去了,因此咱們能夠嘗試一些其餘的方案,好比 Java8 中的 Stream API 和 Java9 中的集合工廠等。
使用 Java8 中的 Stream API 替代,示例以下。原代碼:
List<String> list = new ArrayList() {{ add("Java"); add("Redis"); }}; 複製代碼
替代代碼:
List<String> list = Stream.of("Java", "Redis").collect(Collectors.toList()); 複製代碼
使用集合工廠的 of
方法替代,示例以下。原代碼:
Map map = new HashMap() {{ put("map1", "value1"); put("map2", "value2"); }}; 複製代碼
替代代碼:
Map map = Map.of("map1", "Java", "map2", "Redis"); 複製代碼
顯然使用 Java9 中的方案很是適合咱們,簡單又酷炫,只惋惜咱們還在用 Java 6...6...6... 心碎了一地。
本文咱們講了雙花括號初始化由於會持有外部類的引用,從而能夠會致使內存泄漏的問題,還從字節碼以及反射的層面演示了這個問題。
要想保證雙花括號初始化不會出現內存泄漏的辦法也很簡單,只須要被 static
修飾便可,但這樣作仍是存在潛在的風險,可能會被某人不當心刪除掉,因而咱們另尋它道,發現了可使用 Java8 中的 Stream 或 Java9 中的集合工廠 of
方法替代「{{」。
原創不易,點個「贊」再走唄!
參考 & 鳴謝
cloud.tencent.com/developer/a…
關注公衆號「Java中文社羣」回覆「乾貨」,獲取 50 篇原創乾貨 Top 榜。