1. 程式人生 > >迴歸Java基礎,詳解 Java 內部類

迴歸Java基礎,詳解 Java 內部類

640?wx_fmt=png

今日科技快訊

近日,一篇《估值175億的馬蜂窩 竟是一座殭屍和水軍構成的鬼城?》在網路上刷屏。今天中午,馬蜂窩方面對此作出迴應。馬蜂窩一位市場經理告訴表示,“我們現在正在核實相關情況。我們正在等結果,然後將對媒體反饋。”她表示,暫時不知道具體什麼時候出結果,不過今天應該是可以的,具體釋出渠道在討論中。

作者簡介

大家週一好,新的一週要繼續加油哦!

本篇來自 指點 的投稿,分享了關於 Java內部類講解,一起來看看!希望大家喜歡。

指點 的部落格地址:

https://blog.csdn.net/Hacker_ZhiDian

前言

內部類在 Java 裡面算是非常常見的一個功能了,在日常開發中我們肯定多多少少都用過,這裡總結一下關於 Java 中內部類的相關知識點和一些使用內部類時需要注意的點。

正文

從種類上說,內部類可以分為四類:普通內部類、靜態內部類、匿名內部類、區域性內部類。我們來一個個看:

普通內部類

這個是最常見的內部類之一了,其定義也很簡單,在一個類裡面作為類的一個欄位直接定義就可以了,例:

public class InnerClassTest {

    public class InnerClassA {

    }
}

在這裡 InnerClassA 類為 InnerClassTest 類的普通內部類,在這種定義方式下,普通內部類物件依賴外部類物件而存在,即在建立一個普通內部類物件時首先需要建立其外部類物件,我們在建立上面程式碼中的 InnerClassA 物件時先要建立 InnerClassTest 物件,例:

public class InnerClassTest {

    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;

    public InnerClassTest() {
        // 在外部類物件內部,直接通過 new InnerClass(); 建立內部類物件
        InnerClassA innerObj = new InnerClassA();
        System.out.println("建立 " + this.getClass().getSimpleName() + " 物件"
);
        System.out.println("其內部類的 field1 欄位的值為: " + innerObj.field1);
        System.out.println("其內部類的 field2 欄位的值為: " + innerObj.field2);
        System.out.println("其內部類的 field3 欄位的值為: " + innerObj.field3);
        System.out.println("其內部類的 field4 欄位的值為: " + innerObj.field4);
    }

    public class InnerClassA {
        public int field1 = 1;
        protected int field2 = 2;
        int field3 = 3;
        private int field4 = 4;
//        static int field5 = 5; // 編譯錯誤!普通內部類中不能定義 static 屬性

        public InnerClassA() {
            System.out.println("建立 " + this.getClass().getSimpleName() + " 物件");
            System.out.println("其外部類的 field1 欄位的值為: " + field1);
            System.out.println("其外部類的 field2 欄位的值為: " + field2);
            System.out.println("其外部類的 field3 欄位的值為: " + field3);
            System.out.println("其外部類的 field4 欄位的值為: " + field4);
        }
    }

    public static void main(String[] args) {
        InnerClassTest outerObj = new InnerClassTest();
        // 不在外部類內部,使用:外部類物件. new 內部類構造器(); 的方式建立內部類物件
//        InnerClassA innerObj = outerObj.new InnerClassA();
    }
}

這裡的內部類就像外部類宣告的一個屬性欄位一樣,因此其的物件時依附於外部類物件而存在的,我們來看一下結果:

640?wx_fmt=png

我們注意到,內部類物件可以訪問外部類物件中所有訪問許可權的欄位,同時,外部類物件也可以通過內部類的物件引用來訪問內部類中定義的所有訪問許可權的欄位,後面我們將從原始碼裡面分析具體的原因。 

我們下面來看一下靜態內部類。

靜態內部類

我們知道,一個類的靜態成員獨立於這個類的任何一個物件存在,只要在具有訪問許可權的地方,我們就可以通過 類名.靜態成員名 的形式來訪問這個靜態成員,同樣的,靜態內部類也是作為一個外部類的靜態成員而存在,建立一個類的靜態內部類物件不需要依賴其外部類物件。例:

public class InnerClassTest {
    public int field1 = 1;

    public InnerClassTest() {
        System.out.println("建立 " + this.getClass().getSimpleName() + " 物件");
        // 建立靜態內部類物件
        StaticClass innerObj = new StaticClass();
        System.out.println("其內部類的 field1 欄位的值為: " + innerObj.field1);
        System.out.println("其內部類的 field2 欄位的值為: " + innerObj.field2);
        System.out.println("其內部類的 field3 欄位的值為: " + innerObj.field3);
        System.out.println("其內部類的 field4 欄位的值為: " + innerObj.field4);
    }

    static class StaticClass {

        public int field1 = 1;
        protected int field2 = 2;
        int field3 = 3;
        private int field4 = 4;
        // 靜態內部類中可以定義 static 屬性
        static int field5 = 5;

        public StaticClass() {
            System.out.println("建立 " + StaticClass.class.getSimpleName() + " 物件");
//            System.out.println("其外部類的 field1 欄位的值為: " + field1); // 編譯錯誤!!
        }
    }

    public static void main(String[] args) {
        // 無需依賴外部類物件,直接建立內部類物件
//        InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass();
        InnerClassTest outerObj = new InnerClassTest();
    }
}

結果:

640?wx_fmt=png

可以看到,靜態內部類就像外部類的一個靜態成員一樣,建立其物件無需依賴外部類物件(訪問一個類的靜態成員也無需依賴這個類的物件,因為它是獨立於所有類的物件的)。但是於此同時,靜態內部類中也無法訪問外部類的非靜態成員,因為外部類的非靜態成員是屬於每一個外部類物件的,而本身靜態內部類就是獨立外部類物件存在的,所以靜態內部類不能訪問外部類的非靜態成員,而外部類依然可以訪問靜態內部類物件的所有訪問許可權的成員,這一點和普通內部類無異。

匿名內部類

匿名內部類有多種形式,其中最常見的一種形式莫過於在方法引數中新建一個介面物件 / 類物件,並且實現這個介面宣告 / 類中原有的方法了:

public class InnerClassTest {

    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;

    public InnerClassTest() {
        System.out.println("建立 " + this.getClass().getSimpleName() + " 物件");
    }
    // 自定義介面
    interface OnClickListener {
        void onClick(Object obj);
    }

    private void anonymousClassTest() {
        // 在這個過程中會新建一個匿名內部類物件,
        // 這個匿名內部類實現了 OnClickListener 介面並重寫 onClick 方法
        OnClickListener clickListener = new OnClickListener() {
            // 可以在內部類中定義屬性,但是隻能在當前內部類中使用,
            // 無法在外部類中使用,因為外部類無法獲取當前匿名內部類的類名,
            // 也就無法建立匿名內部類的物件
            int field = 1;

            @Override
            public void onClick(Object obj) {
                System.out.println("物件 " + obj + " 被點選");
                System.out.println("其外部類的 field1 欄位的值為: " + field1);
                System.out.println("其外部類的 field2 欄位的值為: " + field2);
                System.out.println("其外部類的 field3 欄位的值為: " + field3);
                System.out.println("其外部類的 field4 欄位的值為: " + field4);
            }
        };
        // new Object() 過程會新建一個匿名內部類,繼承於 Object 類,
        // 並重寫了 toString() 方法
        clickListener.onClick(new Object() {
            @Override
            public String toString() {
                return "obj1";
            }
        });
    }

    public static void main(String[] args) {
        InnerClassTest outObj = new InnerClassTest();
        outObj.anonymousClassTest();
    }
}

來看看結果:

640?wx_fmt=png

上面的程式碼中展示了常見的兩種使用匿名內部類的情況: 

  1. 直接 new 一個介面,並實現這個介面宣告的方法,在這個過程其實會建立一個匿名內部類實現這個介面,並重寫介面宣告的方法,然後再建立一個這個匿名內部類的物件並賦值給前面的 OnClickListener 型別的引用; 

  2. new 一個已經存在的類 / 抽象類,並且選擇性的實現這個類中的一個或者多個非 final 的方法,這個過程會建立一個匿名內部類物件繼承對應的類 / 抽象類,並且重寫對應的方法。

同樣的,在匿名內部類中可以使用外部類的屬性,但是外部類卻不能使用匿名內部類中定義的屬性,因為是匿名內部類,因此在外部類中無法獲取這個類的類名,也就無法得到屬性資訊。

區域性內部類

區域性內部類使用的比較少,其宣告在一個方法體 / 一段程式碼塊的內部,而且不在定義類的定義域之內便無法使用,其提供的功能使用匿名內部類都可以實現,而本身匿名內部類可以寫得比它更簡潔,因此區域性內部類用的比較少。來看一個區域性內部類的小例子:

public class InnerClassTest {

    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;

    public InnerClassTest() {
        System.out.println("建立 " + this.getClass().getSimpleName() + " 物件");
    }

    private void localInnerClassTest() {
        // 區域性內部類 A,只能在當前方法中使用
        class A {
            // static int field = 1; // 編譯錯誤!區域性內部類中不能定義 static 欄位
            public A() {
                System.out.println("建立 " + A.class.getSimpleName() + " 物件");
                System.out.println("其外部類的 field1 欄位的值為: " + field1);
                System.out.println("其外部類的 field2 欄位的值為: " + field2);
                System.out.println("其外部類的 field3 欄位的值為: " + field3);
                System.out.println("其外部類的 field4 欄位的值為: " + field4);
            }
        }
        A a = new A();
        if (true) {
            // 區域性內部類 B,只能在當前程式碼塊中使用
            class B {
                public B() {
                    System.out.println("建立 " + B.class.getSimpleName() + " 物件");
                    System.out.println("其外部類的 field1 欄位的值為: " + field1);
                    System.out.println("其外部類的 field2 欄位的值為: " + field2);
                    System.out.println("其外部類的 field3 欄位的值為: " + field3);
                    System.out.println("其外部類的 field4 欄位的值為: " + field4);
                }
            }
            B b = new B();
        }
//        B b1 = new B(); // 編譯錯誤!不在類 B 的定義域內,找不到類 B,
    }

    public static void main(String[] args) {
        InnerClassTest outObj = new InnerClassTest();
        outObj.localInnerClassTest();
    }
}

同樣的,在區域性內部類裡面可以訪問外部類物件的所有訪問許可權的欄位,而外部類卻不能訪問區域性內部類中定義的欄位,因為區域性內部類的定義只在其特定的方法體 / 程式碼塊中有效,一旦出了這個定義域,那麼其定義就失效了,就像程式碼註釋中描述的那樣,即外部類不能獲取區域性內部類的物件,因而無法訪問區域性內部類的欄位。最後看看執行結果:

640?wx_fmt=png

內部類的巢狀

  • 內部類的巢狀,即為內部類中再定義內部類,這個問題從內部類的分類角度去考慮比較合適: 

  • 普通內部類:在這裡我們可以把它看成一個外部類的普通成員方法,在其內部可以定義普通內部類(巢狀的普通內部類),但是無法定義 static 修飾的內部類,就像你無法在成員方法中定義 static 型別的變數一樣,當然也可以定義匿名內部類和區域性內部類;

  • 靜態內部類:因為這個類獨立於外部類物件而存在,我們完全可以將其拿出來,去掉修飾它的 static 關鍵字,他就是一個完整的類,因此在靜態內部類內部可以定義普通內部類,也可以定義靜態內部類,同時也可以定義 static 成員;

  • 匿名內部類:和普通內部類一樣,定義的普通內部類只能在這個匿名內部類中使用,定義的區域性內部類只能在對應定義域內使用;

  • 區域性內部類:和匿名內部類一樣,但是巢狀定義的內部類只能在對應定義域內使用。

深入理解內部類

不知道小夥伴們對上面的程式碼有沒有產生疑惑:非靜態內部類可以訪問外部類所有訪問許可權修飾的欄位(即包括了 private 許可權的),同時,外部類也可以訪問內部類的所有訪問許可權修飾的欄位。而我們知道,private 許可權的欄位只能被當前類本身訪問。然而在上面我們確實在程式碼中直接訪問了對應外部類 / 內部類的 private 許可權的欄位,要解除這個疑惑,只能從編譯出來的類下手了,為了簡便,這裡採用下面的程式碼進行測試:

public class InnerClassTest {

    int field1 = 1;
    private int field2 = 2;

    public InnerClassTest() {
        InnerClassA inner = new InnerClassA();
        int v = inner.x2;
    }

    public class InnerClassA {
        int x1 = field1;
        private int x2 = field2;
    }
}

我在外部類中定義了一個預設訪問許可權(同一個包內的類可以訪問)的欄位 field1, 和一個 private 許可權的欄位 field2 ,並且定義了一個內部類 InnerClassA ,並且在這個內部類中也同樣定義了兩個和外部類中定義的相同修飾許可權的欄位,並且訪問了外部類對應的欄位。最後在外部類的構造方法中我定義了一個方法內變數賦值為內部類中 private 許可權的欄位。我們用 javac 命令(javac InnerClassTest.java)編譯這個 .java 檔案,會得到兩個 .classs 檔案。InnerClassTest.class 和 InnerClassTest$InnerClassA.class,我們再用 javap -c 命令(javap -c InnerClassTest 和 javap -c InnerClassTest$InnerClassA)分別反編譯這兩個 .class 檔案,InnerClassTest.class 的位元組碼如下:

640?wx_fmt=png

我們注意到位元組碼中多了一個預設修飾許可權並且名為 access$100 的靜態方法,其接受一個 InnerClassTest 型別的引數,即其接受一個外部類物件作為引數,方法內部用三條指令取到引數物件的 field2 欄位的值並返回。由此,我們現在大概能猜到內部類物件是怎麼取到外部類的 private 許可權的欄位了:就是通過這個外部類提供的靜態方法。 

類似的,我們注意到 24 行位元組碼指令 invokestatic ,這裡代表執行了一個靜態方法,而後面的註釋也寫的很清楚,呼叫的是 InnerClassTest$InnerClassA.access$000 方法,即呼叫了內部類中名為 access$000 的靜態方法,根據我們上面的外部類位元組碼規律,我們也能猜到這個方法就是內部類編譯過程中編譯器自動生成的,那麼我們趕緊來看一下 InnerClassTest$InnerClassA 類的位元組碼吧: 

640?wx_fmt=png

果然,我們在這裡發現了名為 access$000 的靜態方法,並且這個靜態方法接受一個 InnerClassTest$InnerClassA 型別的引數,方法的作用也很簡單:返回引數代表的內部類物件的 x2 欄位值。 

我們還注意到編譯器給內部類提供了一個接受 InnerClassTest 型別物件(即外部類物件)的構造方法,內部類本身還定義了一個名為 this$0 的 InnerClassTest 型別的引用,這個引用在構造方法中指向了引數所對應的外部類物件。 

最後,我們在 25 行位元組碼指令發現:內部類的構造方法通過 invokestatic 指令執行外部類的 access$100 靜態方法(在 InnerClassTest 的位元組碼中已經介紹了)得到外部類物件的 field2 欄位的值,並且在後面賦值給 x2 欄位。這樣的話內部類就成功的通過外部類提供的靜態方法得到了對應外部類物件的 field2 。

上面我們只是對普通內部類進行了分析,但其實匿名內部類和區域性內部類的原理和普通內部類是類似的,只是在訪問上有些不同:外部類無法訪問匿名內部類和區域性內部類物件的欄位,因為外部類根本就不知道匿名內部類 / 區域性內部類的型別資訊(匿名內部類的類名被隱匿,區域性內部類只能在定義域內使用)。但是匿名內部類和區域性內部類卻可以訪問外部類的私有成員,原理也是通過外部類提供的靜態方法來得到對應外部類物件的私有成員的值。而對於靜態內部類來說,因為其實獨立於外部類物件而存在,因此編譯器不會為靜態內部類物件提供外部類物件的引用,因為靜態內部類物件的建立根本不需要外部類物件支援。但是外部類物件還是可以訪問靜態內部類物件的私有成員,因為外部類可以知道靜態內部類的型別資訊,即可以得到靜態內部類的物件,那麼就可以通過靜態內部類提供的靜態方法來獲得對應的私有成員值。來看一個簡單的程式碼證明:

public class InnerClassTest {

    int field1 = 1;
    private int field2 = 2;

    public InnerClassTest() {
        InnerClassA inner = new InnerClassA();
        int v = inner.x2;
    }

    // 這裡改成了靜態內部類,因而不能訪問外部類的非靜態成員
    public static class InnerClassA {
        private int x2 = 0;
    }
}

同樣的編譯步驟,得到了兩個 .class 檔案,這裡看一下內部類的 .class 檔案反編譯的位元組碼 InnerClassTest$InnerClassA:

640?wx_fmt=png

仔細看一下,確實沒有找到指向外部類物件的引用,編譯器只為這個靜態內部類提供了一個無參構造方法。 

而且因為外部類物件需要訪問當前類的私有成員,編譯器給這個靜態內部類生成了一個名為 access$000 的靜態方法,作用已不用我多說了。如果我們不看類名,這個類完全可以作為一個普通的外部類來看,這正是靜態內部類和其餘的內部類的區別所在:靜態內部類物件不依賴其外部類物件存在,而其餘的內部類物件必須依賴其外部類物件而存在。

OK,到這裡問題都得到了解釋:在非靜態內部類訪問外部類私有成員 / 外部類訪問內部類私有成員 的時候,對應的外部類 / 外部類會生成一個靜態方法,用來返回對應私有成員的值,而對應外部類物件 / 內部類物件通過呼叫其內部類 / 外部類提供的靜態方法來獲取對應的私有成員的值。

內部類和多重繼承

我們已經知道,Java 中的類不允許多重繼承,也就是說 Java 中的類只能有一個直接父類,而 Java 本身提供了內部類的機制,這是否可以在一定程度上彌補 Java 不允許多重繼承的缺陷呢?我們這樣來思考這個問題:假設我們有三個基類分別為 A、B、C,我們希望有一個類 D 達成這樣的功能:通過這個 D 類的物件,可以同時產生 A 、B 、C 類的物件,通過剛剛的內部類的介紹,我們也應該想到了怎麼完成這個需求了,建立一個類 D.java:

class A {}

class B {}

class C {}

public class D extends A {

    // 內部類,繼承 B 類
    class InnerClassB extends B {
    }

    // 內部類,繼承 C 類