1. 程式人生 > >Java——內部類詳解

Java——內部類詳解

說起內部類,大家肯定感覺熟悉又陌生,因為一定在很多框架原始碼中有看到別人使用過,但又感覺自己使用的比較少,今天我就帶你具體來看看內部類。

內部類基礎

所謂內部類就是在類的內部繼續定義其他內部結構類。

在 Java 中,廣泛意義上的內部類一般來說包括這四種:成員內部類、區域性內部類、匿名內部類和靜態內部類。下面就先來了解一下這四種內部類的用法。

成員內部類

成員內部類是最普通的內部類,它的定義為位於另一個類的內部,具體使用如下:

class Circle {
    double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            System.out.println("drawshape");
        }
    }
}

這樣看起來,類 Draw 像是類 Circle 的一個成員, Circle 稱為外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括 private 成員和靜態成員),例如:

class Circle {
    private double radius = 0;
    public static int count =1;
    public Circle(double radius) {
        this.radius = radius;
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
            // 外部類的靜態成員
            System.out.println(count);
        }
    }
}

不過要注意的是,當成員內部類擁有和外部類同名的成員變數或者方法時,會發生隱藏現象,即預設情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要採取以下形式進行訪問:

外部類.this.成員變數
外部類.this.成員方法

雖然成員內部類可以無條件地訪問外部類的成員,而外部類想訪問成員內部類的成員卻不是這麼隨心所欲了。在外部類中如果要訪問成員內部類的成員,必須先建立一個成員內部類的物件,再通過指向這個物件的引用來訪問,其具體形式為:

class Circle {
    private double radius = 0;

    public Circle(double radius) {
        this.radius = radius;
        // 必須先建立成員內部類的物件,再進行訪問
        getDrawInstance().drawSahpe();
    }

    private Draw getDrawInstance() {
        return new Draw();
    }

    /**
     * 內部類
     */
    class Draw {
        public void drawSahpe() {
            // 外部類的private成員
            System.out.println(radius);
        }
    }
}

成員內部類是依附外部類而存在的,也就是說,如果要建立成員內部類的物件,前提是必須存在一個外部類的物件。建立成員內部類物件的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        // 第一種方式
        Outter outter = new Outter();
        // 必須通過Outter物件來建立
        Outter.Inner inner = outter.new Inner();

        // 第二種方式
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}

class Outter {
    private Inner inner = null;
    public Outter() {
    }

    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }

    class Inner {
        public Inner() {
        }
    }
}

內部類可以擁有 private 訪問許可權、 protected 訪問許可權、 public 訪問許可權及包訪問許可權。

比如上面的例子,如果成員內部類 Inner 用 private 修飾,則只能在外部類的內部訪問;如果用 public 修飾,則任何地方都能訪問;如果用 protected 修飾,則只能在同一個包下或者繼承外部類的情況下訪問;如果是預設訪問許可權,則只能在同一個包下訪問。

這一點和外部類有一點不一樣,外部類只能被 public 和包訪問兩種許可權修飾。

我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種許可權修飾。

區域性內部類

區域性內部類是定義在一個方法或者一個作用域裡面的類,它和成員內部類的區別在於區域性內部類的訪問僅限於方法內或者該作用域內。

class People{
    public People() {
    }
}

class Man{
    public Man(){
    }

    public People getWoman(){
        /**
         * 區域性內部類
         */
        class Woman extends People{
            int age =0;
        }
        return new Woman();
    }
}

注意,區域性內部類就像是方法裡面的一個區域性變數一樣,是不能用 public 、 protected 、 private 以及 static 修飾的。

匿名內部類

匿名內部類應該是平時我們編寫程式碼時用得最多的,比如建立一個執行緒的時候:

class Test {

    public static void main(String[] args) {
        Thread thread = new Thread(
                // 匿名內部類
                new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Thread run");
                    }
                }
        );
    }
}

同樣的,匿名內部類也是不能有訪問修飾符和 static 修飾符的。

匿名內部類是唯一一種沒有構造器的類。正因為其沒有構造器,所以匿名內部類的使用範圍非常有限,大部分匿名內部類用於介面回撥。

匿名內部類在編譯的時候由系統自動起名為Outter$1.class。一般來說,匿名內部類用於繼承其他類或是實現介面,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。

靜態內部類

靜態內部類也是定義在另一個類裡面的類,只不過在類的前面多了一個關鍵字 static 。

靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非 static 成員變數或者方法,這點很好理解,因為在沒有外部類的物件的情況下,可以建立靜態內部類的物件,如果允許訪問外部類的非 static 成員就會產生矛盾,因為外部類的非 static 成員必須依附於具體的物件。

例如:

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {
    }

    /**
     * 靜態
     */
    static class Inner {
        public Inner() {
        }
    }
}

深入理解內部類

通過上面的介紹,相比你已經大致瞭解的內部類的使用,那麼你的心裡想必會有一個疑惑:

為什麼成員內部類可以無條件訪問外部類的成員?

首先我們先定義一個內部類:

public class Outter {
    private Inner inner = null;

    public Outter() {
    }

    public Inner getInnerInstance() {
        if (inner == null)
            inner = new Inner();
        return inner;
    }

    protected class Inner {
        public Inner() {
        }
    }
}

先用 javac 進行編譯,你可以發現會生成兩個檔案: Outter$Inner.class 和 Outter.class 。接下來利用javap -p反編譯 Outter$Inner.class ,其結果如下:

Classfile /D:/project/Test/src/test/java/test/Outter$Inner.class
  Last modified 2019-11-25; size 408 bytes
  MD5 checksum b936e37bc77059b83951429e28f3f225
  Compiled from "Outter.java"
public class Outter$Inner
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#13         // test/Outter$Inner.this$0:Ltest/Outter;
   #2 = Methodref          #4.#14         // java/lang/Object."<init>":()V
   #3 = Class              #16            // test/Outter$Inner
   #4 = Class              #19            // java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               Ltest/Outter;
   #7 = Utf8               <init>
   #8 = Utf8               (Ltest/Outter;)V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               SourceFile
  #12 = Utf8               Outter.java
  #13 = NameAndType        #5:#6          // this$0:Ltest/Outter;
  #14 = NameAndType        #7:#20         // "<init>":()V
  #15 = Class              #21            // test/Outter
  #16 = Utf8               test/Outter$Inner
  #17 = Utf8               Inner
  #18 = Utf8               InnerClasses
  #19 = Utf8               java/lang/Object
  #20 = Utf8               ()V
  #21 = Utf8               test/Outter
{
  final Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public Outter$Inner(Outter);
    descriptor: (Ltest/Outter;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 16: 0
        line 17: 9
}
SourceFile: "Outter.java"
InnerClasses:
     protected #17= #3 of #15; //Inner=class test/Outter$Inner of class test/Outter

32行的內容為:final Outter this$0;

學過 C 的朋友應該能知道,這是一個指向外部類 Outter 物件的指標,也就是說編譯器會預設為成員內部類新增一個指向外部類物件的引用,這樣也就解釋了為什麼成員內部類能夠無條件訪問外部類了。

那麼這個引用是如何賦初值的呢?下面接著看內部類的構造器:public Outter$Inner(Outter);

從這裡可以看出,雖然我們在定義的內部類的構造器是無參構造器,但編譯器還是會預設新增一個引數,該引數的型別為指向外部類物件的一個引用,所以成員內部類中的 Outter this&0 指標便指向了外部類物件,因此可以在成員內部類中隨意訪問外部類的成員。

從這裡也間接說明了成員內部類是依賴於外部類的,如果沒有建立外部類的物件,則無法對 Outter this&0 引用進行初始化賦值,也就無法建立成員內部類的物件了。

為什麼區域性內部類和匿名內部類只能訪問區域性final變數?

我們還是採用和之前一樣的解答方式,先定義一個類:

public class Outter {

    public static void main(String[] args)  {
        Outter outter = new Outter();
        int b = 10;
        outter.test(b);
    }

    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

通過 javac 編譯 Outter,也會生成兩個檔案: Outter.class 和 Outter1.class。預設情況下,編譯器會為匿名內部類和區域性內部類起名為 Outter$x.class( x 為正整數)。

根據我提供的類,可以思考一個問題:

當 test 方法執行完畢之後,變數 a 的生命週期就結束了,而此時 Thread 物件的生命週期很可能還沒有結束,那麼在 Thread 的 run 方法中繼續訪問變數 a 就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?

Java 採用了複製的手段來解決這個問題。將 Outter$1.class 反編譯可以得到下面的內容:

Classfile /D:/project/Test/src/test/java/test/Outter$1.class
  Last modified 2019-11-25; size 653 bytes
  MD5 checksum 2e238dafbd73356eba22d473c6469082
  Compiled from "Outter.java"
class test.Outter$1 extends java.lang.Thread
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Fieldref           #6.#23         // test/Outter$1.this$0:Ltest/Outter;
   #2 = Fieldref           #6.#24         // test/Outter$1.val$b:I
   #3 = Methodref          #7.#25         // java/lang/Thread."<init>":()V
   #4 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(I)V
   #6 = Class              #30            // test/Outter$1
   #7 = Class              #32            // java/lang/Thread
   #8 = Utf8               val$b
   #9 = Utf8               I
  #10 = Utf8               this$0
  #11 = Utf8               Ltest/Outter;
  #12 = Utf8               <init>
  #13 = Utf8               (Ltest/Outter;I)V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               run
  #17 = Utf8               ()V
  #18 = Utf8               SourceFile
  #19 = Utf8               Outter.java
  #20 = Utf8               EnclosingMethod
  #21 = Class              #33            // test/Outter
  #22 = NameAndType        #34:#35        // test:(I)V
  #23 = NameAndType        #10:#11        // this$0:Ltest/Outter;
  #24 = NameAndType        #8:#9          // val$b:I
  #25 = NameAndType        #12:#17        // "<init>":()V
  #26 = Class              #36            // java/lang/System
  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
  #28 = Class              #39            // java/io/PrintStream
  #29 = NameAndType        #40:#35        // println:(I)V
  #30 = Utf8               test/Outter$1
  #31 = Utf8               InnerClasses
  #32 = Utf8               java/lang/Thread
  #33 = Utf8               test/Outter
  #34 = Utf8               test
  #35 = Utf8               (I)V
  #36 = Utf8               java/lang/System
  #37 = Utf8               out
  #38 = Utf8               Ljava/io/PrintStream;
  #39 = Utf8               java/io/PrintStream
  #40 = Utf8               println
{
  final int val$b;
    descriptor: I
    flags: ACC_FINAL, ACC_SYNTHETIC

  final test.Outter this$0;
    descriptor: Ltest/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  test.Outter$1(test.Outter, int);
    descriptor: (Ltest/Outter;I)V
    flags:
    Code:
      stack=2, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Ltest/Outter;
         5: aload_0
         6: iload_2
         7: putfield      #2                  // Field val$b:I
        10: aload_0
        11: invokespecial #3                  // Method java/lang/Thread."<init>":()V
        14: return
      LineNumberTable:
        line 10: 0

  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
         8: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: getfield      #2                  // Field val$b:I
        15: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        18: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 18
}
SourceFile: "Outter.java"
EnclosingMethod: #21.#22                // test.Outter.test
InnerClasses:
     #6; //class test/Outter$1

我們看到在 run 方法中有一條指令:bipush 10

這條指令表示將運算元10壓棧,表示使用的是一個本地區域性變數。

這個過程是在編譯期間由編譯器預設進行,如果這個變數的值在編譯期間可以確定,則編譯器預設會在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。

這樣一來,匿名內部類使用的變數是另一個區域性變數,只不過值和方法中區域性變數的值相等,因此和方法中的區域性變數完全獨立開。

接下來也來看一下 test.Outter$1 的構造方法:test.Outter$1(test.Outter, int);

我們看到匿名內部類 Outter$1 的構造器含有兩個引數,一個是指向外部類物件的引用,一個是 int 型變數,很顯然,這裡是將變數 test 方法中的形參 b 以引數的形式傳進來對匿名內部類中的拷貝(變數 b 的拷貝)進行賦值初始化。

也就說如果區域性變數的值在編譯期間就可以確定,則直接在匿名內部裡面建立一個拷貝。如果區域性變數的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。

從上面可以看出,在 run 方法中訪問的變數 b 根本就不是test方法中的區域性變數 b 。這樣一來就解決了前面所說的 生命週期不一致的問題。但是新的問題又來了,既然在 run 方法中訪問的變數 b 和test方法中的變數 b 不是同一個變數,那麼當在 run 方法中改變變數 b 的值的話,會出現什麼情況?

會造成資料不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題, Java 編譯器就限定必須將變數 b 限制為 final ,不允許對變數 b 進行更改(對於引用型別的變數,是不允許指向新的物件),這樣資料不一致性的問題就得以解決了。

到這裡,想必大家應該清楚為何 方法中的區域性變數和形參都必須用 final 進行限定了。

靜態內部類有特殊的地方嗎?

從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不建立外部類物件的情況下建立內部類的物件。

另外,靜態內部類是不持有指向外部類物件的引用的,這個讀者可以自己嘗試反編譯 class 檔案看一下就知道了,是沒有 Outter this&0 引用的。

總結

今天介紹了內部類相關的知識,包括其一般的用法以及內部類和外部類的依賴關係,通過對位元組碼進行反編譯詳細瞭解了其實現模式,最後留給大家一個任務自己去實際探索一下靜態內部類的實現。希望通過這篇介紹可以幫大家更加深刻了解內部類。

有興趣的話可以訪問我的部落格或者關注我的公眾號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/

相關推薦

Java部類

strong 匿名 per 創建 show rac 成員變量 end outer 成員內部類(聲明在類內部且方法外的):1是外部類的一個成員:①可以有修飾符(4個)②static final ③可以調用外部類的屬性、方法

【轉】Java部類

一、內部類基礎   在Java中,可以將一個類定義在另一個類裡面或者一個方法裡面,這樣的類稱為內部類。廣泛意義上的內部類一般來說包括這四種:成員內部類、區域性內部類、匿名內部類和靜態內部類。下面就先來了解一下這四種內部類的用法。     1、成員內部類   成員內部類是最普通的內部類,它的定

Java部類 及 區域性部類和匿名部類只能訪問區域性final變數的原因

說起內部類這個詞,想必很多人都不陌生,但是又會覺得不熟悉。原因是平時編寫程式碼時可能用到的場景不多,用得最多的是在有事件監聽的情況下,並且即使用到也很少去總結內部類的用法。今天我們就來一探究竟。下面是本文的目錄大綱:   一.內部類基礎   二.深入理解內部類   三.內部類的使用場景和好處   

Java 部類及其練習

學習心得 一、專業課 1、內部類 1.內部類 1.1 是指在一個外部類的內部再定義一個類,類名不需要和文件夾相同 1.2內部類可以是靜態static的,也可用public,default,protected和private修飾(而外部類只能使用 public和default

Java——部類

說起內部類,大家肯定感覺熟悉又陌生,因為一定在很多框架原始碼中有看到別人使用過,但又感覺自己使用的比較少,今天我就帶你具體來看看內部類。 內部類基礎 所謂內部類就是在類的內部繼續定義其他內部結構類。 在 Java 中,廣泛意義上的內部類一般來說包括這四種:成員內部類、區域性內部類、匿名內部類和靜態內部類

JAVA】的部類

轉載部落格: https://www.cnblogs.com/dolphin0520/p/3811445.html 作者:海 子   說起內部類這個詞,想必很多人都不陌生,但是又會覺得不熟悉。原因是平時編寫程式碼時可能用到的場景不多

Java筆記之內部類、匿名部類

內部類 內部類訪問特點: 1、內部類可以直接訪問外部類中的成員 2、外部類要訪問內部類,必須建立內部類的物件 class Outer { private int num=3; class Inner //內部類 { void s

“全棧2019”Java第七十七章:抽象部類與抽象靜態部類

難度 初級 學習時間 10分鐘 適合人群 零基礎 開發語言 Java 開發環境 JDK v11 IntelliJ IDEA v2018.3 文章原文連結 “全棧2019”Java第七十七章:抽象內部類與抽象靜態內部類詳解 下一章 “全棧2019”Java第七十八章:內部

java四種部類

一般來說,有4中內部類:常規內部類、靜態內部類、區域性內部類、匿名內部類。  一.常規內部類:常規內部類沒有用static修飾且定義在在外部類類體中。   1.常規內部類中的方法可以直接使用外部類的例項變數和例項方法。   2.在常規內部類中可以直接用內部類建立物件  

Java中的部類,為什麼需要部類

內部類的共性   內部類分為: 成員內部類、靜態巢狀類、方法內部類、匿名內部類。      (1)、內部類仍然是一個獨立的類,在編譯之後內部類會被編譯成獨立的.class檔案,但是前面冠以外部類的類

Java面試題-匿名部類

前言 匿名內部類應該是屬於java基礎的知識點,後來我們在開發中使用的也不算很少了,只是我們可能沒太注意自己所建立的或者使用的一些類就是匿名內部類,我看了排名很靠前的一些關於匿名內部類的一些 部落格講解的都很棒,只可惜跳躍的很大導致很多人不多看幾次很難理

java部類(1):java部類的建立以及對外提供的訪問方式,匿名部類

前言 我們在描述事物的時候,事物的內部還有事物,這個內部事物還要訪問外部事物中的內容時。那麼,這個內部事物就可以用內部類來描述。內部類也叫內建類,巢狀類。 正文 一,內部類的形式以及對外訪問的方式 顧名思義,內部類就是一個類巢狀在另一個類中。內部類可

[JAVA基礎]部類

public class OuterClass { private String sex; public static String name = "chenssy"; /** *靜態內部類 */ static class InnerClass

“全棧2019”Java第七十三章:外部類裡多個靜態非靜態部類

難度 初級 學習時間 10分鐘 適合人群 零基礎 開發語言 Java 開發環境 JDK v11 IntelliJ IDEA v2018.3 文章原文連結 “全棧2019”Java第七十三章:外部類裡多個靜態非靜態內部類詳解

“全棧2019”Java第九十四章:區域性部類

難度 初級 學習時間 10分鐘 適合人群 零基礎 開發語言 Java 開發環境 JDK v11 IntelliJ IDEA v2018.3 文章原文連結 “全棧2019”Java第九十四章:區域性內部類詳解 下一章 “全棧2019”Java第九十五章:方法中可以定義靜態

java匿名部類

【宣告】此文轉載自:http://blog.csdn.net/zhandoushi1982/article/details/8778487 ——感謝分享,尊重作者,交流無限! 記得JAVA中抽象類是不能建立例項的,但是在程式碼中總會看見new 抽象類名的用法。如

Java之匿名部類

表示 div -h UNC 花括號 繼承 匿名對象 對象 但是 前言 本文講解Java中最後一種內部類,叫做匿名內部類。顧名思義,所謂的匿名內部類就是一個沒有顯式的名字的內部類,在實際開發中,此種內部類用的是非常多的。 匿名內部類 本質:匿名內部類會隱式的繼承一個類或

Java_51_組合_部類_字串(String類)_equals和==的區別

組合 使用組合,可以獲得更多的靈活性,你甚至可以在執行的時候才決定哪幾個類組合在一起。 使用繼承,他是一種高度耦合,派生類和基類被緊緊的綁在一起,靈活性大大降低,而且,濫用繼承,也會使繼承樹變得又大又複雜,很難理解和維護。 如果是is-a關係,用繼承。【是一個[物件]】 如果是h

javaSE之各種部類

文章目錄 成員內部類 靜態內部類 方法內部類 匿名內部類 在java中,內部類主要分為四種: 成員內部類 靜態內部類 方法內部類 匿名內部類 成員內部類 成員內部類要

部類部類,匿名部類

外部類: 最普通的,我們平時見到的那種類,就是在一個字尾為.java的檔案中, 直接定義的類,比如 public class Student {   private String name;   private int age; } 內部類: 內部類,顧名思義,就是包含