Java程序員必備基礎:內部類解析

前言

整理了一下內部類的相關知識,算是比較全,比較基礎的,但願你們一塊兒學習進步。 html

1、什麼是內部類?

在Java中,能夠將一個類的定義放在另一個類的定義內部,這就是內部類。內部類自己就是類的一個屬性,與其餘屬性 定義方式一致。java

一個內部類的例子:android

public class Outer {

    private int radius = 1;
    public static int count = 2;

    public Outer() {
    }

    class inner{
        public void visitOuter() {
            System.out.println("visit outer private member variable:" + radius);
            System.out.println("visit outer static variable:" + count);
        }
    }
}
複製代碼

2、內部類的種類

內部類能夠分爲四種:成員內部類、局部內部類、匿名內部類和靜態內部類面試

靜態內部類

定義在類內部的靜態類,就是靜態內部類。算法

public class Outer {

    private static int radius = 1;

    static class StaticInner {
        public void visit() {
            System.out.println("visit outer static variable:" + radius);
        }
    }
}
複製代碼

靜態內部類能夠訪問外部類全部的靜態變量,而不可訪問外部類的非靜態變量;靜態內部類的建立方式,new 外部類.靜態內部類(),以下:數據庫

Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
複製代碼

成員內部類

定義在類內部,成員位置上的非靜態類,就是成員內部類。編程

public class Outer {

    private static  int radius = 1;
    private int count =2;
    
     class Inner {
        public void visit() {
            System.out.println("visit outer static variable:" + radius);
            System.out.println("visit outer variable:" + count);
        }
    }
}
複製代碼

成員內部類能夠訪問外部類全部的變量和方法,包括靜態和非靜態,私有和公有。成員內部類依賴於外部類的實例,它的建立方式外部類實例.new 內部類(),以下:設計模式

Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
複製代碼

局部內部類

定義在方法中的內部類,就是局部內部類。bash

public class Outer {

    private  int out_a = 1;
    private static int STATIC_b = 2;

    public void testFunctionClass(){
        int inner_c =3;
        class Inner {
            private void fun(){
                System.out.println(out_a);
                System.out.println(STATIC_b);
                System.out.println(inner_c);
            }
        }
        Inner  inner = new Inner();
        inner.fun();
    }
    public static void testStaticFunctionClass(){
        int d =3;
        class Inner {
            private void fun(){
                // System.out.println(out_a); 編譯錯誤,定義在靜態方法中的局部類不能夠訪問外部類的實例變量
                System.out.println(STATIC_b);
                System.out.println(d);
            }
        }
        Inner  inner = new Inner();
        inner.fun();
    }
}
複製代碼

定義在實例方法中的局部類能夠訪問外部類的全部變量和方法,定義在靜態方法中的局部類只能訪問外部類的靜態變量和方法。局部內部類的建立方式,在對應方法內,new 內部類(),以下:ide

public static void testStaticFunctionClass(){
    class Inner {
    }
    Inner  inner = new Inner();
 }
複製代碼

匿名內部類

匿名內部類就是沒有名字的內部類,平常開發中使用的比較多。

public class Outer {

    private void test(final int i) {
        new Service() {
            public void method() {
                for (int j = 0; j < i; j++) {
                    System.out.println("匿名內部類" );
                }
            }
        }.method();
    }
 }
 //匿名內部類必須繼承或實現一個已有的接口 
 interface Service{
    void method();
}
複製代碼

除了沒有名字,匿名內部類還有如下特色:

  • 匿名內部類必須繼承一個抽象類或者實現一個接口。
  • 匿名內部類不能定義任何靜態成員和靜態方法。
  • 當所在的方法的形參須要被匿名內部類使用時,必須聲明爲 final。
  • 匿名內部類不能是抽象的,它必需要實現繼承的類或者實現的接口的全部抽象方法。

匿名內部類建立方式:

new 類/接口{ 
  //匿名內部類實現部分
}
複製代碼

3、內部類的優勢

咱們爲何要使用內部類呢?由於它有如下優勢:

  • 一個內部類對象能夠訪問建立它的外部類對象的內容,包括私有數據!
  • 內部類不爲同一包的其餘類所見,具備很好的封裝性;
  • 內部類有效實現了「多重繼承」,優化 java 單繼承的缺陷。
  • 匿名內部類能夠很方便的定義回調。

一個內部類對象能夠訪問建立它的外部類對象的內容,包括私有數據!

public class Outer {

    private  int radius = 1;
    
    protected void test(){
        System.out.println("我是外部類方法");
    }

    class Inner {
        public void visit() {
            System.out.println("訪問外部類變量" + radius);
            test();
        }
    }
}
複製代碼

咱們能夠看到,內部類Inner是能夠訪問外部類Outer的私有變量radius或者方法test的。

內部類不爲同一包的其餘類所見,具備很好的封裝性

當內部類使用 private修飾時,這個類就對外隱藏了。當內部類實現某個接口,而且進行向上轉型,對外部來講,接口的實現已經隱藏起來了,很好體現了封裝性。

//提供的接口
interface IContent{
    String getContents();
}

public class Outer {
     //私有內部類屏蔽實現細節
     private class PContents implements IContent{
         @Override
         public String getContents() {
             System.out.println("獲取內部類內容");
             return "內部類內容";
         }
     }

    //對外提供方法
    public IContent getIContent() {
        return new PContents();
    }

    public static void main(String[] args) {
        Outer outer=new Outer();
        IContent a1=outer.getIContent();
        a1.getContents();
    }
}

複製代碼

咱們能夠發現,Outer外部類對外提供方法getIContent,用內部類實現細節,再用private修飾內部類,屏蔽起來,把Java的封裝性表現的淋漓盡致。

內部類有效實現了「多重繼承」,優化 java 單繼承的缺陷。

咱們知道Java世界中,一個類只能有一個直接父類,即以單繼承方式存在。可是內部類讓「多繼承」成爲可能:

  • 通常來講,內部類繼承某個類或者實現某個接口,內部類的代碼操做建立它的外圍類的對象。內部類提供了某種進入其外圍類的窗口。
  • 每一個內部類均可以隊裏的繼承自一個(接口的)實現,因此不管外圍類是否已經繼承了某個(接口的)實現,對於內部類沒有影響
  • 接口解決了部分問題,一個類能夠實現多個接口,內部類容許繼承多個非接口類型(類或抽象類)。

一份來自Java編程思想,內部類實現「多繼承」的溫暖以下:

class D {}
abstract class E{}
class Z extends D {
E makeE(){ return new E() {}; }
}

public class MultiImplementation {
static void takesD(D d) {}
static void takesE(E e) {}
public static void main(String[] args){
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
} 
複製代碼

代碼中出現了一個類D,一個抽象類E。而後,用類Z繼承D,內部類構造返回E。所以,當你無論要的是D仍是E,Z均可以應付,「多繼承」的特色完美表現出來。

匿名內部類能夠很方便的定義回調。

什麼是回調?假設有兩個類A和B,在A中調用B的一個方法b,而b在執行又調用了A的方法c,則c就稱爲回調函數。

固然,回調函數也能夠是a函數,這就是 同步回調,最簡單的回調方式。 回調應用場景挺多的,如android中的事件監聽器。匿名內部類能夠很方便的定義回調,看個例子

//定義一個CallBack接口
public interface CallBack {
    void execute();
}

public class TimeTools {

    /**
     * 測試函數調用時長,經過定義CallBack接口的execute方法
     * @param callBack
     */
    public   void  testTime(CallBack callBack) {
        long  beginTime = System.currentTimeMillis(); //記錄起始時間
        callBack.execute(); ///進行回調操做
        long  endTime = System.currentTimeMillis(); //記錄結束時間
        System.out.println("[use time]:"  + (endTime - beginTime)); //打印使用時間
    }

    public   static   void  main(String[] args) {
        TimeTools tool = new  TimeTools();
        tool.testTime(new  CallBack(){
            //匿名內部類,定義execute方法
            public   void  execute(){
                TestTimeObject testTimeObject = new TestTimeObject();
                testTimeObject.testMethod();
            }
        });
    }
}

複製代碼

在調用testTime()測時間的時候,用匿名內部類實現一個方法execute(),在該方法內搞事情(執行目標函數),執行完後,又回到testTime方法,很好了實現測試函數調用時長的功能。顯然,匿名內部類讓回調實現變得簡單

4、內部類的底層

內部類標誌符

每一個內部類都會產生一個.class文件,其中包含了如何建立該類型的對象的所有信息。內部類也必須生成一個.class文件以包含它們的Class對象信息。內部類文件的命名有嚴格規則:外圍類的名字+$+內部類的名字。

一個簡單例子:

public class Outer {
    class Inner{
    }
}
複製代碼

javac Outer.java編譯完成後, 生成的class文件以下:

若是內部類是匿名的,編譯器會簡單地產生一個數字做爲其標識符。若是內部類是嵌套在別的內部類之中(靜態內部類),只需直接將它們的名字加在其外圍類標誌符與「$」的後面。

爲何內部類能夠訪問外部類的成員,包括私有數據?

由上一小節,咱們知道內部類能夠訪問外部類的成員,包括私有數據。那麼它是怎麼作到的呢?接下來揭曉答案。

先看這個簡單地例子:

public class Outer {

    private int i = 0;
    
    class Inner{
        void method(){
            System.out.println(i);
        }
    }
}
複製代碼

一個外部類Outer,一個外部類私有屬性i,一個內部類Inner,一個內部類方法method。內部類方法訪問了外部類屬性i。

先編譯,javac Outer.java,生成.class文件,以下:

javap -classpath . -v Outer$Inner,反編譯Outter$Inner.class文件獲得如下信息:

咱們能夠看到這一行,它是一個指向外部類對象的指針:

final innerclass.Outer this$0;
複製代碼

雖然編譯器在建立內部類時爲它加上了一個指向外部類的引用, 可是這個引用是怎樣賦值的呢?編譯器會爲內部類的構造方法添加一個參數,進行初始化, 參數的類型就是外部類的類型,以下:

innerclass.Outer$Inner(innerclass.Outer);
複製代碼

成員內部類中的Outter this&0 指針便指向了外部類對象,所以能夠在成員內部類中隨意訪問外部類的成員。

局部內部類和匿名內部類訪問局部變量的時候,爲何變量必需要加上final?

局部內部類和匿名內部類訪問局部變量的時候,爲何變量必需要加上final呢?它內部原理是什麼呢?

先看這段代碼:

public class Outer {

    void outMethod(){
        final int a =10;
        class Inner {
            void innerMethod(){
                System.out.println(a);
            }

        }
    }
}
複製代碼

反編譯(Outer$1Inner)獲得如下信息

咱們在內部類innerMethod方法中,能夠看到如下這條指令:

3: bipush   10
複製代碼
  • 它表示將常量10壓入棧中,表示使用的是一個本地局部變量。
  • 其實,若是一個變量的值在編譯期間能夠肯定(demo中肯定是10了),則編譯器會默認在匿名內部類(局部內部類)的常量池中添加一個內容相等的字面量或直接將相應的字節碼嵌入到執行字節碼中。
  • 醬紫能夠確保局部內部類使用的變量與外層的局部變量區分開,它們只是值相等而已。

以上例子,爲何要加final呢?是由於生命週期不一致, 局部變量直接存儲在棧中,當方法執行結束後,非final的局部變量就被銷燬。而局部內部類對局部變量的引用依然存在,若是局部內部類要調用局部變量時,就會出錯。加了final,能夠確保局部內部類使用的變量與外層的局部變量區分開,解決了這個問題。

咱們再來看一段代碼,其實就是把變量a挪到傳參方式進來

public class Outer {

    void outMethod(final int a){
        class Inner {
            void innerMethod(){
                System.out.println(a);
            }
        }
    }
}
複製代碼

反編譯可得

咱們看到匿名內部類Outer$1Inner的構造器含有兩個參數,一個是指向外部類對象的引用,一個是int型變量,很顯然,這裏是將變量innerMethod方法中的形參a以參數的形式傳進來對匿名內部類中的拷貝(變量a的拷貝)進行賦值初始化。

那麼,新的問題又來了,既然在innerMethod方法中訪問的變量a和outMethod方法中的變量a不是同一個變量,當在innerMethod方法中修改a會怎樣?那就會形成數據不一致的問題了。

怎麼解決呢?使用final修飾符,final修飾的引用類型變量,不容許指向新的對象,這就解決數據不一致問題。注意: 在Java8 中,被局部內部類引用的局部變量,默認添加final,因此不須要添加final關鍵詞。

5、內部類的應用場景。

通常咱們在哪些場景下使用內部類呢?

場景之一:一些多算法場合

一些算法多的場合,也能夠藉助內部類,如:

Arrays.sort(emps,new Comparator(){
  Public int compare(Object o1,Object o2)
  {
   return ((Employee)o1).getServedYears()-((Employee)o2).getServedYears();
  }
});
複製代碼

場景二:解決一些非面向對象的語句塊。

若是一些語句塊,包括if…else語句,case語句等等比較多,很差維護擴展,那麼就能夠藉助內部類+設計模式解決。

場景之三:適當使用內部類,使得代碼更加靈活和富有擴展性。

適當的使用內部類,可使得你的代碼更加靈活和富有擴展性。如JDK的lamda表達式,用內部類很是多,代碼優雅不少。以下

// JDK8 Lambda表達式寫法
new Thread(() -> System.out.println("Thread run()")).start();
複製代碼

場景四:當某個類除了它的外部類,再也不被其餘的類使用時。

若是一個類,不能爲其餘的類使用;或者出於某種緣由,不能被其餘類引用。那咱們就能夠考慮把它實現爲內部類。數據庫鏈接池就是這樣一個典型例子。

6、內部類常見面試題

最後,咱們來看一道經典內部類面試題吧。

public class Outer {
    private int age = 12;

    class Inner {
        private int age = 13;
        public void print() {
            int age = 14;
            System.out.println("局部變量:" + age);
            System.out.println("內部類變量:" + this.age);
            System.out.println("外部類變量:" + Outer.this.age);
        }
    }

    public static void main(String[] args) {
        Outer.Inner in = new Outer().new Inner();
        in.print();
    }

}

複製代碼

運行結果:

參考與感謝

我的公衆號

  • 若是你是個愛學習的好孩子,能夠關注我公衆號,一塊兒學習討論。
  • 若是你以爲本文有哪些不正確的地方,能夠評論,也能夠關注我公衆號,私聊我,你們一塊兒學習進步哈。
相關文章
相關標籤/搜索