Java內部類的實現原理與可能的記憶體洩漏說明
在使用java內部類的時候要注意可能引起的記憶體洩漏
程式碼如下
package com.example; public class MyClass { public static void main(String[] args) throws Throwable { } public class A{ public void methed1(){ } } public static class B{ public void methed1(){ } }
編譯生成了如下檔案
反編譯MyClass
反編譯MyClassA
反編譯GlassB
從反編譯的結果可以知道,內部類的實現其實是通過編譯器的語法糖實現的,通過生成相應的子類即以OutClassName$InteriorClassName命名的Class檔案。
並新增建構函式,在建構函式中傳入外部類,這也是為什麼內部類能使用外部類的方法與欄位的原因。
我們明白了這個也就要小心,當外部類與內部類生命週期不一致的時候很有可能發生記憶體洩漏,例如在一個Activity啟動一個Thread執行一個任務,因為Thread是內部類持有了Activity的引用,當Activity銷燬的時候如果Thread的任務沒有執行完成,造成Activity的引用不能釋放,Activity不能被釋放而引起了記憶體洩漏。
這種情況下可以通過宣告一個static的內部類來解決問題,從反編譯中可以看出,宣告為static的類不會持有外部類的引用,如果你想使用外部類的話,可以通過軟引用的方式儲存外部類的引用。
具體的程式碼就不上了。
補充知識:Java內部類的底層實現原理
摘要:
定義:在一個類中建立另一個類,叫做成員內部類,這個內部類可以是靜態的,也可以是非靜態的。
已知靜態內部類的應用(可以解決的問題):
通過內部類解決java 的單繼承問題,外部類不能同時繼承的類可以交給內部類繼承
設計模式中,builder 模式通過定義一個靜態內部類實現
型別彙總:
靜態內部類
成員內部類
方法內部類
匿名內部類
一、靜態內部類
靜態內部類的定義和普通的靜態變數或者靜態方法的定義方法是一樣的,使用static關鍵字,只不過這次static是修飾在class上的,一般而言,只有靜態內部類才允許使用static關鍵字修飾,普通類的定義是不能用static關鍵字修飾的,這一點需要注意一下。下面定義一個靜態內部類:
public class Out { private static String name; private int age; public static class In{ private int age; public void sayHello(){ System.out.println("my name is : "+name); //--編譯報錯--- //System.out.println("my age is :"+ age); } } }
在上述程式碼中,In這個類就是一個靜態內部類。我們說內部類是可以訪問外部類的私有欄位和私有方法的,對於靜態內部類,它遵循一致的原則,只能訪問外部類的靜態成員。上述程式碼中,外部類的非靜態私有欄位age在靜態內部類中使不允許訪問的,而靜態欄位name則是可訪問的。下面我們看,如何建立一個靜態內部類的例項物件。
public static void main(String [] args){ Out.In innerClass = new Out.In(); innerClass.sayHello(); }
靜態內部類的例項物件建立還是比較簡潔的,不同於成員內部類,它不需要關聯外部類例項(具體的下文介紹),下面我們再看一段程式碼:
public class Out { private static String name; public static class In{ public void sayHello(){ System.out.println(name); showName(); } } private static void showName(){ System.out.println(name); } }
上述程式碼在內部類中兩次訪問了外部類的靜態成員,第一次訪問了靜態欄位name,第二次訪問的靜態方法showName。在我們反編譯這個類之前,首先需要知道的是,所謂的內部類的概念只是出現在編譯階段,對於jvm層是沒有內部類這個概念的。也就是說,編譯器會將一個類編譯成一個原始檔,對於內部類也是一樣,它會從它的外部類中抽離出來,增加一些與外部類的聯絡,然後被編譯成一個單獨的原始檔。下面我們先編譯執行之後,利用Dj反編譯class檔案看看編譯器都做了些什麼事情。
//這是我們的Out外部類 public class Out { //省去了一些不重要的部分 private static void showName() { System.out.println(name); } private static String name; static String access$000(){return name;} static void access$100(){showName();} } //這是我們的內部類 public static class Out$In { public void sayHello() { System.out.println(Out.access$000()); Out.access$100(); } public Out$In() { } }
相信大家也已經看出來這兩者之間的某種聯絡,編譯器將Out這個類編譯成兩個獨立的class原始檔。對於Out中所有的私有成員(也就是內部類分離出去之後不能訪問的成員),增設了可供呼叫的access$xxx方法,從而實現內部類與外部類之間的聯絡。這就是他們的本質。
至於使用場景,一般來說,對於和外部類聯絡緊密但是並不依賴於外部類例項的情況下,可以考慮定義成靜態內部類。下面我們看稍顯複雜的成員內部類。
二、成員內部類
我們說了,四種不同型別的內部類都各自有各自的使用場景,靜態內部類適合於那種和外部類關係密切但是並不依賴外部類例項的情況。但是對於需要和外部類例項相關聯的情況下,可以選擇將內部類定義成成員內部類。以下程式碼定義了一個簡單的成員內部類:
public class Out { private String name; public void showName(){ System.out.println("my name is : "+name); } public class In{ public void sayHello(){ System.out.println(name); Out.this.showName(); } } }
以上定義了一個簡單的內部類In,我們的成員內部類可以直接訪問外部類的成員欄位和成員方法,因為它是關聯著一個外部類例項的。下面我們看看在外部是如何建立該內部類例項的。
public static void main(String [] args){ Out out = new Out(); Out.In in = out.new In(); in.sayHello(); }
因為成員內部類是關聯著一個具體的外部類例項的,所以它的例項建立必然是由外部類例項來建立的。對於例項的建立,我們只需要記住即可,成員內部類的例項建立需要關聯外部類例項物件,靜態內部類例項建立相對簡單。下面我們主要看看在編譯階段編譯器是如何保持內部類對外部類成員資訊可訪問的。
//反編譯的Out外部類原始碼 public class Out { //省略部分非核心程式碼 public void showName() { System.out.println((new StringBuilder()).append("my name is : ").append(name).toString()); } private String name; static String access$000(Out o){return o.name;} } //反編譯的內部類In原始碼 public class Out$In { public void sayHello() { System.out.println(Out.access$000(Out.this)); showName(); } final Out this$0; public Out$In() { this.this$0 = Out.this; super(); } }
由上述程式碼其實我們可以知道,當我們利用外部類例項建立內部類例項的時候,會將外部類例項作為初始資源傳入內部類構造過程。這樣我們就可以通過該例項訪問外部類所有的成員資訊,包括私有成員。(顯式增加了暴露方法)
至於使用場景,對於那種要高度依賴外部類例項的情況下,定義一個成員內部類則會顯的更加明智。
三、方法內部類
方法內部類,顧名思義,定義在一個方法內部的類。方法內部類相對而言要複雜一些,下面定義一個方法內部類:
public class Out { private String name; public void sayHello(){ class In{ public void showName(){ System.out.println("my name is : "+name); } } In in = new In(); in.showName(); } }
我們定義了一個類,在該類中又定義了一個方法sayHello,然而在該方法中我們定義了一個內部類,類In就是一個方法內部類。我們的方法內部類的生命週期不超過包含它的方法的生命週期,也就是說,方法內部類只能在方法中使用。所以在宣告的時候,任何的訪問修飾符都是沒有意義的,於是Java乾脆不允許使用任何的訪問修飾符修飾方法內部類。其中還需要注意一點的是,定義和使用時兩回事,別看那一大串定義類的程式碼,你實際想要使用該類,就必須new物件,而對於方法內部類而言,只能在方法內部new物件。這就是方法內部類的簡單介紹,下面我們看看其實現原理。
有關方法內部類的實現原理其實是和成員內部類差不太多的,也是在內部類初始化的時候為其傳入一個外部類例項,區別在哪呢?就在於方法內部類是定義在具體方法的內部的,所以該類除了可以通過傳入的外部例項訪問外部類中的欄位和方法,對於包含它的方法中被傳入的引數也會隨著外部類例項一起初始化給內部類。
毋庸置疑的是,方法內部類的封裝性比之前介紹的兩種都要完善。所以一般只有在需要高度封裝的時候才會將類定義成方法內部類。
四、匿名內部類
可能內部類的所有分類中,匿名內部類的名號是最大的,也是我們最常用到的,多見於函數語言程式設計,lambda表示式等。下面我們重點看看這個匿名內部類。
匿名內部類就是沒有名字的內部類,在定義完成同時,例項也建立好了,常常和new關鍵字緊密結合。當然,它也不侷限於類,也可以是介面 ,可以出現在任何位置。下面我們定義一個匿名內部類:
//首先定義一個普通類 public class Out { private String name; private void sayHello(){ System.out.println("my name is :" + name); } } //定義和使用一個匿名內部類 public static void main(String [] args){ Out out = new Out(){ @Override public void sayHello(){ System.out.println("my name is cyy"); } public void showName(){ System.out.println("hello single"); } }; out.sayHello(); }
從上述程式碼中可以很顯然的讓我們看出來,我們的匿名內部類必定是要依託一個父類的,因為它是沒有名字的,無法用一個具體的型別來表示。所以匿名內部類往往都是通過繼承一個父類,重寫或者重新宣告一些成員來實現一個匿名內部類的定義。實際上還是利用了裡式轉換原理。
從中我們也可以看到,一個匿名內部類定義的完成就意味著該內部類例項建立的完成。下面我們看看其實現原理:
//反編譯出來的匿名內部類 static class Test$1 extends Out { Out out; public void sayHello() { System.out.println("my name is cyy"); } Test$1(Out o) { this.out = o; } }
其實在看了上述三種內部類的原理之後,反而覺得匿名內部類的實現較為簡單了。主要思路還是將內部類抽離出來,通過初始化傳入外部類的例項以達到對外部類所有成員的訪問。只是在匿名內部類中,被依託的父類不是他的外部類。匿名內部類的主要特點在於,沒有名字,物件只能被使用一次,可以出現在任意位置。所以它的使用場景也是呼之欲出,對於一些對程式碼簡潔度有所要求的情況下,可首選匿名內部類。
以上完成了對四種內部類的簡單介紹,對於他們各自實現的原理也都已經介紹過了。其實大致相同,由於jvm對每個類都要求一個單獨的原始碼檔案,所以編譯階段就完成了分離的操作,但是在分離的過程中又要保持內部類和外部類之間的這種聯絡,於是編譯器添加了一些介面保持這種資訊共享的結構。使用內部類可以大大增加程式的封裝性,使得程式碼整體簡潔度較高。
希望能給大家一個參考,也希望大家多多支援我們。