背景
今天面試了一道考題 就說類部類 有什么作用 在什么場景下使用 下面就來分享一下吧 :
內部類的作用
- 1.可以無條件地訪問外圍類的所有元素
- 2.實現隱藏
- 3.可以實現多重繼承
- 4.通過匿名內部類來優化簡單的接口實現
我們為什么需要內部類?或者說內部類為啥要存在?其主要原因有如下幾點:
- 內部類方法可以訪問該類定義所在作用域中的數據,包括被 private 修飾的私有數據
- 內部類可以對同一包中的其他類隱藏起來
- 內部類可以解決 java 單繼承的缺陷
- 當我們想要定義一個回調函數卻不想寫大量代碼的時候我們可以選擇使用匿名內部類來實現
1.可以無條件地訪問外圍類的所有元素
為什么可以引用訪問外部類:
內部類雖然和外部類寫在同一個文件中, 但是編譯完成后, 還是生成各自的class文件,內部類通過this訪問外部類的成員。
1 編譯器自動為內部類添加一個成員變量, 這個成員變量的類型和外部類的類型相同, 這個成員變量就是指向外部類對象(this)的引用;
2 編譯器自動為內部類的構造方法添加一個參數, 參數的類型是外部類的類型, 在構造方法內部使用這個參數為內部類中添加的成員變量賦值;
3 在調用內部類的構造函數初始化內部類對象時,會默認傳入外部類的引用。
編譯指令 javac classpath(.java文件的路徑)
反編譯指令 javap -v(詳細信息) classpath(.class文件的路徑)
/** * 內部類無條件訪問外部類元素 */ public class DataOuterClass { private String data = "外部類數據"; private class InnerClass { public InnerClass() { System.out.println(data); } } public void getInner() { new InnerClass(); } public static void main(String[] args) { DataOuterClass outerClass = new DataOuterClass(); outerClass.getInner(); } }
輸出結果:
外部類數據
data這是在DataOuterClass定義的私有變量。這個變量在內部類中可以無條件地訪問.
2.實現隱藏
關於內部類的第二個好處其實很顯而易見,我們都知道外部類即普通的類(其他包下調用)不能使用 private protected 訪問權限符來修飾的,而內部類則可以使用 private 和 protected 來修飾。當我們使用 private 來修飾內部類的時候這個類就對外隱藏了。這看起來沒什么作用,但是當內部類實現某個接口的時候,在進行向上轉型,對外部來說,就完全隱藏了接口的實現了
接口
public interface InnerInterface { void innerMethod(); }
具體類
/** * 實現信息隱藏 */ public class OuterClass { /** * private修飾內部類,實現信息隱藏 */ private class InnerClass implements InnerInterface { @Override public void innerMethod() { System.out.println("實現內部類隱藏"); } } public InnerInterface getInner() { return new InnerClass(); } }
調用程序
public class Test { public static void main(String[] args) { OuterClass outerClass = new OuterClass(); InnerInterface inner = outerClass.getInner(); inner.innerMethod(); } }
輸出結果:
實現內部類隱藏
從這段代碼里面我們知道OuterClass的getInner()方法能返回一個InnerInterface接口實例但我並不知道這個實例是這么實現的。
而且由於InnerClass是private的,所以我們如果不看代碼的話根本看不到這個具體類的名字,所以說它可以很好的實現隱藏。
3.可以實現多重繼承
我們知道 java 是不允許使用 extends 去繼承多個類的。內部類的引入可以很好的解決這個事情。
我的理解 Java只能繼承一個類這個學過基本語法的人都知道,而在有內部類之前它的多重繼承方式是用接口來實現的。但使用接口有時候有很多不方便的地方。比如我們實現一個接口就必須實現它里面的所有方法。
而有了內部類就不一樣了。它可以使我們的類繼承多個具體類或抽象類。如下面這個例子:
類一
public class ExampleOne { public String name() { return "inner"; } }
類二
public class ExampleTwo { public int age() { return 25; } }
類三
public class MainExample { /** * 內部類1繼承ExampleOne */ private class InnerOne extends ExampleOne { public String name() { return super.name(); } } /** * 內部類2繼承ExampleTwo */ private class InnerTwo extends ExampleTwo { public int age() { return super.age(); } } public String name() { return new InnerOne().name(); } public int age() { return new InnerTwo().age(); } public static void main(String[] args) { MainExample mi = new MainExample(); System.out.println("姓名:" + mi.name()); System.out.println("年齡:" + mi.age()); } }
大家注意看類三,里面分別實現了兩個內部類 InnerOne,和InnerTwo ,InnerOne類又繼承了ExampleOne,InnerTwo繼承了ExampleTwo,這樣我們的類三MainExample就擁有了ExampleOne和ExampleTwo的方法和屬性,也就間接地實現了多繼承。
4.通過匿名內部類來優化簡單的接口實現
關於匿名內部類相信大家都不陌生,我們常見的點擊事件的寫法就是這樣的:
不用再去實現OnClickListener對象了。
... view.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(){ // ... do XXX... } }) ...
內部類與外部類的關系
-
對於非靜態內部類,內部類的創建依賴外部類的實例對象,在沒有外部類實例之前是無法創建內部類的
-
內部類是一個相對獨立的實體,與外部類不是is-a關系
-
創建內部類的時刻並不依賴於外部類的創建
對於普通內部類創建方法有兩種:
public class ClassOuter { public void fun(){ System.out.println("外部類方法"); } public class InnerClass{ } } public class TestInnerClass { public static void main(String[] args) { //創建方式1 ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass(); //創建方式2 ClassOuter outer = new ClassOuter(); ClassOuter.InnerClass inner = outer.new InnerClass(); } }
內部類的分類
內部類可以分為:靜態內部類(嵌套類)和非靜態內部類。非靜態內部類又可以分為:成員內部類、方法內部類、匿名內部類。
靜態內部類和非靜態內部類的區別
我們通過一個例子就可以很好的理解這幾點區別:
public class ClassOuter { private int noStaticInt = 1; private static int STATIC_INT = 2; public void fun() { System.out.println("外部類方法"); } public class InnerClass { //static int num = 1; 此時編輯器會報錯 非靜態內部類則不能有靜態成員 public void fun(){ //非靜態內部類的非靜態成員可以訪問外部類的非靜態變量。 System.out.println(STATIC_INT); System.out.println(noStaticInt); } } public static class StaticInnerClass { static int NUM = 1;//靜態內部類可以有靜態成員 public void fun(){ System.out.println(STATIC_INT); //System.out.println(noStaticInt); 此時編輯器會報 不可訪問外部類的非靜態變量錯 } } } public class TestInnerClass { public static void main(String[] args) { //非靜態內部類 創建方式1 ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass(); //非靜態內部類 創建方式2 ClassOuter outer = new ClassOuter(); ClassOuter.InnerClass inner = outer.new InnerClass(); //靜態內部類的創建方式 ClassOuter.StaticInnerClass staticInnerClass = new ClassOuter.StaticInnerClass(); } }
局部內部類
定義
如果一個內部類只在一個方法中使用到了,那么我們可以將這個類定義在方法內部,這種內部類被稱為局部內部類。其作用域僅限於該方法。
局部內部類有兩點值得我們注意的地方:
- 局部內部類不允許使用訪問權限修飾符 public private protected 均不允許
- 局部內部類對外完全隱藏,除了創建這個類的方法可以訪問它其他的地方是不允許訪問的。
- 局部內部類與成員內部類不同之處是他可以引用成員變量,但該成員必須聲明為 final,並內部不允許修改該變量的值。(這句話並不准確,因為如果不是基本數據類型的時候,只是不允許修改引用指向的對象,而對象本身是可以被就修改的)
public class ClassOuter { private int noStaticInt = 1; private static int STATIC_INT = 2; public void fun(final int params) { System.out.println("外部類方法"); } public void testFunctionClass(){ class FunctionClass{ private void fun(){ System.out.println("局部內部類的輸出"); System.out.println(STATIC_INT); System.out.println(noStaticInt); System.out.println(params); //params ++ ; // params 不可變所以這句話編譯錯誤 } } FunctionClass functionClass = new FunctionClass(); functionClass.fun(); } }
- 匿名內部類是沒有訪問修飾符的。
- 匿名內部類必須繼承一個抽象類或者實現一個接口
- 匿名內部類中不能存在任何靜態成員或方法
- 匿名內部類是沒有構造方法的,因為它沒有類名。
- 與局部內部類相同匿名內部類也可以引用局部變量。此變量也必須聲明為 final
public class Button { public void click(final int params){ //匿名內部類,實現的是ActionListener接口 new ActionListener(){ public void onAction(){ System.out.println("click action..." + params); } }.onAction(); } //匿名內部類必須繼承或實現一個已有的接口 public interface ActionListener{ public void onAction(); } public static void main(String[] args) { Button button=new Button(); button.click(); } }
為什么局部變量需要final修飾呢
因為局部變量和匿名內部類的生命周期不同。
匿名內部類是創建后是存儲在堆中的,而方法中的局部變量是存儲在Java棧中,當方法執行完畢后,就進行退棧,同時局部變量也會消失。
那么此時匿名內部類還有可能在堆中存儲着,那么匿名內部類要到哪里去找這個局部變量呢?
為了解決這個問題編譯器為自動地幫我們在匿名內部類中創建了一個局部變量的備份,也就是說即使方法執結束,匿名內部類中還有一個備份,自然就不怕找不到了。
但是問題又來了。
如果局部變量中的a不停的在變化。
那么豈不是也要讓備份的a變量無時無刻的變化。
為了保持局部變量與匿名內部類中備份域保持一致。
編譯器不得不規定死這些局部域必須是常量,一旦賦值不能再發生變化了。
所以為什么匿名內部類應用外部方法的域必須是常量域的原因所在了。
特別注意
在Java8中已經去掉要對final的修飾限制,但其實只要在匿名內部類使用了,該變量還是會自動變為final類型(只能使用,不能賦值
實際開發中內部類有可能會引起的問題
內部類會造成程序的內存泄漏
相信做 Android 的朋友看到這個例子一定不會陌生,我們經常使用的 Handler 就無時無刻不給我們提示着這樣的警告。
我們先來看下內部類為什么會造成內存泄漏。
要想了解為啥內部類為什么會造成內存泄漏我們就必須了解 java 虛擬機的回收機制,但是我們這里不會詳盡的介紹 java 的內存回收機制,我們只需要了解 java 的內存回收機制通過「可達性分析」來實現的。
即 java 虛擬機會通過內存回收機制來判定引用是否可達,如果不可達就會在某些時刻去回收這些引用。
那么內部類在什么情況下會造成內存泄漏的可能呢?
如果一個匿名內部類沒有被任何引用持有,那么匿名內部類對象用完就有機會被回收。
如果內部類僅僅只是在外部類中被引用,當外部類的不再被引用時,外部類和內部類就可以都被GC回收。
如果當內部類的引用被外部類以外的其他類引用時,就會造成內部類和外部類無法被GC回收的情況,即使外部類沒有被引用,因為內部類持有指向外部類的引用)。
public class ClassOuter { Object object = new Object() { public void finalize() { System.out.println("inner Free the occupied memory..."); } }; public void finalize() { System.out.println("Outer Free the occupied memory..."); } } public class TestInnerClass { public static void main(String[] args) { try { Test(); } catch (InterruptedException e) { e.printStackTrace(); } } private static void Test() throws InterruptedException { System.out.println("Start of program."); ClassOuter outer = new ClassOuter(); Object object = outer.object; outer = null; System.out.println("Execute GC"); System.gc(); Thread.sleep(3000); System.out.println("End of program."); } }
運行程序發現 執行內存回收並沒回收 object 對象,
這是因為即使外部類沒有被任何變量引用,只要其內部類被外部類以外的變量持有,外部類就不會被GC回收。
我們要尤其注意內部類被外面其他類引用的情況,這點導致外部類無法被釋放,極容易導致內存泄漏。
在Android 中 Hanlder 作為內部類使用的時候其對象被系統主線程的 Looper (當然這里也可是子線程手動創建的 Looper)掌管的消息隊列 MessageQueue 中的 Hanlder 發送的 Message 持有,當消息隊列中有大量消息處理的需要處理,或者延遲消息需要執行的時候,創建該 Handler 的 Activity 已經退出了,Activity 對象也無法被釋放,這就造成了內存泄漏。
那么 Hanlder 何時會被釋放,當消息隊列處理完 Hanlder 攜帶的 message 的時候就會調用 msg.recycleUnchecked()釋放Message所持有的Handler引用。
在 Android 中要想處理 Hanlder 內存泄漏可以從兩個方面着手:
在關閉Activity/Fragment 的 onDestry,取消還在排隊的Message:
mHandler.removeCallbacksAndMessages(null);
將 Hanlder 創建為靜態內部類並采用軟引用方式
mHandler private static class MyHandler extends Handler { private final WeakReference<MainActivity> mActivity; public MyHandler(MainActivity activity) { mActivity = new WeakReference<MainActivity>(activity); } @Override public void handleMessage(Message msg) { MainActivity activity = mActivity.get(); if (activity == null || activity.isFinishing()) { return; } // ... } }
文章借鑒於掘金......