搞懂 JAVA 內部類

搞懂 JAVA 內部類

前些天寫了一篇關於 2018 年奮鬥計劃的文章,其實作 Android 開發也有一段時間了,文章中所寫的內容,也都是在平常開發中遇到各類問題後總結下來須要鞏固的基礎或者進階知識。那麼本文就從內部類開刀。java

本文將會從如下幾部分來總結:面試

  1. 爲何要存在內部類
  2. 內部類與外部類的關係
  3. 內部的分類及幾種分類的詳細使用注意事項
  4. 實際開發中會遇到內部類的問題

內部類爲何存在

內部類 ( inner class ) : 定義在另外一個類中的類編程

咱們爲何須要內部類?或者說內部類爲啥要存在?其主要緣由有以下幾點:編輯器

  • 內部類方法能夠訪問該類定義所在做用域中的數據,包括被 private 修飾的私有數據
  • 內部類能夠對同一包中的其餘類隱藏起來
  • 內部類能夠實現 java 單繼承的缺陷
  • 當咱們想要定義一個回調函數卻不想寫大量代碼的時候咱們能夠選擇使用匿名內部類來實現

內部類方法能夠訪問該類定義所在做用域中的數據

作 Android 的咱們有時候會將各類 Adapter 直接寫在 Activity 中,如:ide

class MainActivity extends AppCompatActivity{
    ....
    private List<Fragment> fragments = new ArrayList();
    
    private class BottomPagerAdapter extends FragmentPagerAdapter{
        ....
        @Override
        public Fragment getItem(int position) {
            return fragments.get(position);
        }
        ...
    }
    ...
}
複製代碼

上文中 BottomPagerAdapter 即是 MainActivity 的一個內部類。也能夠看出 BottomPagerAdapter 能夠直接訪問 MainActivity 中定義的 fragments 私有變量。若是將 BottomPagerAdapter 不定義爲內部類訪問 fragments 私有變量 沒有 getXXX 方法是作不到的。 這就是內部類的第一點好處。函數

但是爲何內部類就能夠隨意訪問外部類的成員呢?是如何作到的呢?oop

當外部類的對象建立了一個內部類的對象時,內部類對象一定會祕密捕獲一個指向外部類對象的引用,而後訪問外部類的成員時,就是用那個引用來選擇外圍類的成員的。固然這些編輯器已經幫咱們處理了。測試

另外注意內部類只是一種編譯器現象,與虛擬機無關。編譯器會將內部類編譯成 外部類名$內部類名 的常規文件,虛擬機對此一無所知。優化

內部類能夠對同一包中的其餘類隱藏起來

關於內部類的第二個好處其實很顯而易見,咱們都知道外部類即普通的類不能使用 private protected 訪問權限符來修飾的,而內部類則可使用 private 和 protected 來修飾。當咱們使用 private 來修飾內部類的時候這個類就對外隱藏了。這看起來沒什麼做用,可是當內部類實現某個接口的時候,在進行向上轉型,對外部來講,就徹底隱藏了接口的實現了。 如:spa

public interface Incrementable{
  void increment();
}
//具體類
public class Example {

    private class InsideClass implements InterfaceTest{
         public void test(){
             System.out.println("這是一個測試");
         }
    }
    public InterfaceTest getIn(){
        return new InsideClass();
    }
}

public class TestExample {

 public static void main(String args[]){
    Example a=new Example();
    InterfaceTest a1=a.getIn();
    a1.test();
 }
}
複製代碼

從這段代碼裏面我只知道Example的getIn()方法能返回一個InterfaceTest實例但我並不知道這個實例是這麼實現的。並且因爲InsideClass是private的,因此咱們若是不看代碼的話根本看不到這個具體類的名字,因此說它能夠很好的實現隱藏。

內部類能夠實現 java 單繼承的缺陷

咱們知道 java 是不容許使用 extends 去繼承多個類的。內部類的引入能夠很好的解決這個事情。 如下引用 《Thinking In Java》中的一段話:

每一個內部類均可以隊裏的繼承自一個(接口的)實現,因此不管外圍類是否已經繼承了某個(接口的)實現,對於內部類沒有影響 若是沒有內部類提供的、能夠繼承多個具體的或抽象的類的能力,一些設計與編程問題就難以解決。 接口解決了部分問題,一個類能夠實現多個接口,內部類容許繼承多個非接口類型(類或抽象類)。

個人理解 Java只能繼承一個類這個學過基本語法的人都知道,而在有內部類以前它的多重繼承方式是用接口來實現的。但使用接口有時候有不少不方便的地方。好比咱們實現一個接口就必須實現它裏面的全部方法。而有了內部類就不同了。它可使咱們的類繼承多個具體類或抽象類。以下面這個例子:

//類一
public class ClassA {
   public String name(){
       return "liutao";
   }
   public String doSomeThing(){
    // doSomeThing
   }
}
//類二
public class ClassB {
    public int age(){
        return 25;
    }
}

//類三
public class MainExample{
   private class Test1 extends ClassA{
        public String name(){
          return super.name();
        }
    }
    private class Test2 extends ClassB{
       public int age(){
         return super.age();
       }
    }
   public String name(){
    return new Test1().name();
   }
   public int age(){
       return new Test2().age();
   }
   public static void main(String args[]){
       MainExample mi=new MainExample();
       System.out.println("姓名:"+mi.name());
       System.out.println("年齡:"+mi.age());
   }
}
複製代碼

上邊這個例子能夠看出來,MainExample 類經過內部類擁有了 ClassA 和 ClassB 的兩個類的繼承關係。 而無需關注 ClassA 中的 doSomeThing 方法的實現。這就是比接口實現更有戲的地方。

經過匿名內部類來"優化"簡單的接口實現

關於匿名內部類相信你們都不陌生,咱們常見的點擊事件的寫法就是這樣的:

...
    view.setOnClickListener(new View.OnClickListener(){
        @Override
        public void onClick(){
            // ... do XXX...
        }
    })
    ...
複製代碼

爲何標題優化帶了"",其實在 Java8 引入 lambda 表達式以前我我的是比較討厭這種寫法的,由於 onClick 方法中的內容可能很複雜,可能會有不少判斷邏輯的存在,這就致使代碼顯得很累贅,因此我的更喜歡使用匿名內部類來完成一些簡便的操做,配合lambda 表達式,代碼會更便於閱讀 如

view.setOnClickListener(v -> gotoVipOpenWeb());
複製代碼

內部類與外部類的關係

  • 對於非靜態內部類,內部類的建立依賴外部類的實例對象,在沒有外部類實例以前是沒法建立內部類的
  • 內部類是一個相對獨立的實體,與外部類不是is-a關係
  • 建立內部類的時刻並不依賴於外部類的建立

建立內部類的時刻並不依賴於外部類的建立

這句話是《Thinking In Java》中的一句話,大部分人看到這裏會斷章取義的認爲 內部類的建立不依賴於外部類的建立,這種理解是錯誤的,去掉時刻二字這句話就會變了一個味道。

事實上靜態內部類「嵌套類」的確不依賴與外部類的建立,由於 static 並不依賴於實例,而依賴與類 Class 自己。

可是對於普通的內部類,其必須依賴於外部類實例建立正如第一條關係所說:對於非靜態內部類,內部類的建立依賴外部類的實例對象,在沒有外部類實例以前是沒法建立內部類的。

對於普通內部類建立方法有兩種:

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();
    }
}

複製代碼

值得注意的是:正式因爲這種依賴關係,因此普通內部類中不容許有 static 成員,包括嵌套類(內部類的靜態內部類) ,道理顯然而知:static 自己是針對類自己來講的。又因爲非static內部類老是由一個外部的對象生成,既然與對象相關,就沒有靜態的字段和方法。固然靜態內部類不依賴於外部類,因此其內容許有 static 成員。

如今返回頭來看標題,其實英文版中這句話是這樣描述的:

The point of creation of the inner-class objects not tied to the creation of the outer-class object.

我的認爲這句話理解爲:建立一個外部類的時候不必定要建立這個內部類。

拿文章開頭的 Adapter 的例子來講,咱們不能說建立了 Activity 就必定會建立 Adapter (假設 Adapter 建立依賴於某個條件的成立)。只有當知足條件的時候纔會被建立。

內部類是一個相對獨立的實體,與外部類不是is-a關係

首先理解什麼是「is-a關係」: is-a關係是指繼承關係。知道什麼是is-a關係後相信,內部類個外部類不是is-a關係就很容易理解了。

而對於內部類是一個相對獨立的實體,咱們能夠從兩個方面來理解這句話:

  1. 一個外部類能夠擁有多個內部類對象,而他們之間沒有任何關係,是獨立的個體。
  2. 從編譯結果來看,內部類被表現爲 「外部類$內部類.class 」,因此對於虛擬機來講他個一個單獨的類來講沒什麼區別。可是咱們知道他們是有關係的,由於內部類默認持有一個外部類的引用。

內部類的分類

內部類能夠分爲:靜態內部類(嵌套類)和非靜態內部類。非靜態內部類又能夠分爲:成員內部類、方法內部類、匿名內部類。對於這幾種類的書寫相信你們早已熟練,因此本節主要說明的是這幾種類之間的區別:

靜態內部類和非靜態內部類的區別

  1. 靜態內部類能夠有靜態成員,而非靜態內部類則不能有靜態成員。
  2. 靜態內部類能夠訪問外部類的靜態變量,而不可訪問外部類的非靜態變量;
  3. 非靜態內部類的非靜態成員能夠訪問外部類的非靜態變量。
  4. 靜態內部類的建立不依賴於外部類,而非靜態內部類必須依賴於外部類的建立而建立。

咱們經過一個例子就能夠很好的理解這幾點區別:

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();
    }
}

複製代碼

局部內部類

若是一個內部類只在一個方法中使用到了,那麼咱們能夠將這個類定義在方法內部,這種內部類被稱爲局部內部類。其做用域僅限於該方法。

局部內部類有兩點值得咱們注意的地方:

  1. 局部內類不容許使用訪問權限修飾符 public private protected 均不容許
  2. 局部內部類對外徹底隱藏,除了建立這個類的方法能夠訪問它其餘的地方是不容許訪問的。
  3. 局部內部類與成員內部類不一樣之處是他能夠引用成員變量,但該成員必須聲明爲 final,並內部不容許修改該變量的值。(這句話並不許確,由於若是不是基本數據類型的時候,只是不容許修改引用指向的對象,而對象自己是能夠被就修改的)
public class ClassOuter {
    private int noStaticInt = 1;
    private static int STATIC_INT = 2;

    public void fun() {
        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();
    }
}

複製代碼

匿名內部類

  1. 匿名內部類是沒有訪問修飾符的。
  2. 匿名內部類必須繼承一個抽象類或者實現一個接口
  3. 匿名內部類中不能存在任何靜態成員或方法
  4. 匿名內部類是沒有構造方法的,由於它沒有類名。
  5. 與局部內部相同匿名內部類也能夠引用局部變量。此變量也必須聲明爲 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 虛擬機會經過內存回收機制來斷定引用是否可達,若是不可達就會在某些時刻去回收這些引用。

那麼內部類在什麼狀況下會形成內存泄漏的可能呢?

  1. 若是一個匿名內部類沒有被任何引用持有,那麼匿名內部類對象用完就有機會被回收。

  2. 若是內部類僅僅只是在外部類中被引用,當外部類的再也不被引用時,外部類和內部類就能夠都被GC回收。

  3. 若是當內部類的引用被外部類之外的其餘類引用時,就會形成內部類和外部類沒法被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 建立爲靜態內部類並採用軟引用方式
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;
            }
            // ...
        }
    }

複製代碼

總結

本文從內部類的存在理由,內部類與外部類的關係,內部類分類以及開發中內部類可能形成的內存泄漏的問題上,總結了與內部類相關的問題,原諒本人才疏學淺,本文以前想要使用「完全搞懂 java 內部類」可是當我寫完整片文章,我才發現,經過 java 內部類可能會延伸出各類各樣的知識,因此最終去掉了完全二字,總結可能有不少不到位的地方。還望可以及時幫我指出。

其中內部類分類,靜態內部類和非靜態內部類,以及局部內部類和匿名內部的共同點和區別點極可能被面試問到,若是能所以延伸到內部類形成的內存泄漏問題上,想必也是個加分項。

本文參考 《Thinking in java》,《Java 核心技術 卷1》 http://blog.csdn.net/mcryeasy/article/details/54848452 http://blog.csdn.net/mcryeasy/article/details/53149594 https://www.zhihu.com/question/21373020 http://daiguahub.com/2016/09/08/java%E5%86%85%E9%83%A8%E7%B1%BB%E7%9A%84%E6%84%8F%E4%B9%89%E5%92%8C%E4%BD%9C%E7%94%A8/ https://www.zhihu.com/question/20969764

相關文章
相關標籤/搜索