Java 基礎 —— 內部類

Java 基礎 —— 內部類

基礎認識

Java中,將一個類定義在另一個類中,這個就稱爲內部類。
內部類可通俗認爲分爲以下幾類:

  • 成員內部類
  • 局部內部類
  • 匿名內部類
  • 靜態內部類

成員內部類

位於一個類的內部,屬於最基礎的內部類。

public class Circle {
    
    double radius = 0;

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

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

成員內部類訪問外部類

閱讀上述代碼,類Draw像是Circle的一個成員,Circle成爲外部類。成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括private和final修飾成員)。

public class Circle {
    
    private double radius = 0;
    
    public static int count =1;

    private static final String test = "我是靜態私有成員";
    
    public Circle(double radius) {
        this.radius = radius;
    }

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

注意:內部類和外部類存在同名成員變量和方法時,會發生隱藏對象。默認情況會訪問成員內部類的成員,如果要訪問外部類同名成員,語法如下:

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

外部類訪問成員內部類

內部類訪問外部類雖然很方便,但是反之則沒那麼容易,外部類如果想使用內部類的方法和成員,必須先實例化內部類(new),代碼如下:

public class Circle {

    private double radius = 0;

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

    /**
     * 實例化一個內部類
     * @return
     */
    private Draw getDrawInstance() {
        return new Draw();
    }

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

外部如何使用成語內部類

成員內部類是依附外部類進行使用的,如果要使用成員內部類,必須存在一個外部類對象,依託於外部類創建成員內部類,代碼如下:

  • 成員內部類
public class Outter {

    /**
     * 聲明成員內部類,但沒有實例化
     */
    private Inner inner = null;

    /**
     * 外部類構造函數
     */
    public Outter() {

    }

    /**
     * 創建成員內部類
     * @return
     */
    public Inner getInnerInstance() {
        if(inner == null) {
            inner = new Inner();
        }
        return inner;
    }

    /**
     * 成員內部類構造函數
     */
    class Inner {
        public Inner() {
            
        }
    }
}
  • 測試運行代碼
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();
    }
}

成員內部類的訪問權限

  • 成員內部類可以擁有private訪問權限、protected訪問權限、public訪問權限及包訪問權限。
    藉由上述例子舉例:
    • 如果成員內部類 Inner 用 private 修飾,則只能在外部類的內部訪問。
    • 如果成員內部類 Inner 用 public 修飾,則任何地方都能訪問。
    • 如果成員內部類 Inner 用 protected 修飾,則只能在同一個包下,或者繼承外部類的情況下訪問。
    • 如果默認訪問權限(默認是default修飾),則只能在同一個包下訪問。

外部類只允許用public和包訪問兩種權限修飾。

局部內部類

局部內部類是定義在一個方法或者一個作用域裏面的類,與成員內部類的區別在於訪問權限於方法內或者該作用域內,示例代碼如下:

  • People類
class People{

    public static String peopleString = "Frank能活到";

    public People() {

    }

    public String getPeopleString() {
        return peopleString;
    }
}
  • 成員內部類
public class Man {

    public Man(){
    }

    public String getWoman(){
        /**
         * 局部內部類
         */
        class Woman extends People{

            /**
             * 定義年齡
             */
            public int age = 100;

            /**
             * 返回一句話
             * @return
             */
            public String getSuperString() {
                return super.getPeopleString() + 100 + "歲";
            }

        }

        // 直接返回局部內部類裏面的一個函數,如果這裏返回局部內部類,外部無法獲取到內部的成員;
        return new Woman().getSuperString();
    }

    public static void main(String[] args) {
        System.out.println(new Man().getWoman());
    }
}

局部內部類和方法內的局部變量相似,是不能有public、protected、private及static修飾。

匿名內部類

匿名內部類大量應用在研發中。

使用方法一

下面舉例,Android事件監聽:

// 爲button設置點擊監聽,OnClickListener爲匿名內部類。
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});
 
//  爲history_btn設置點擊監聽。
history_btn.setOnClickListener(new OnClickListener() {
     
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});

這兩個按鈕設置了點擊監聽,內部類代碼如下:

new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
    }
}

使用方法二

匿名內部類在使用時前,必須存在。我們也可以像以下示例中的方式使用:

/**
 * 分別爲button、history_btn設置監聽。
 */
private void setListener() {
    button.setOnClickListener(new Listener1());       
    history_btn.setOnClickListener(new Listener2());
}

/**
 * 局部內部類,監聽1
 */
class Listener1 implements View.OnClickListener {
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}
/**
 * 局部內部類,監聽2
 */
class Listener2 implements View.OnClickListener {
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}

雖然方法2可以達到一樣的效果,但是代碼冗長不利於維護,所以一般採用第一種寫法。

匿名內部類的特點

  • 匿名內部類不能有訪問修飾符和static修飾符。
  • 匿名內部類是唯一一種沒有構造器的類。
    因爲沒有構造器,匿名內部類的使用範圍非常有限,大部分匿名內部類用於接口回調。
  • 匿名內部類編譯時虛擬機自動起名爲 Outter$1.class

通常,匿名內部類用於繼承其它類或者實現接口,並不需要增加額外方法,只是對繼承方法對重載或重構。

靜態內部類

同樣是定義在一個類裏面的類,並多了一個static修飾。

靜態內部類的特點

  • 包含static修飾
  • 靜態內部類是不需要依賴外部類的,和類的靜態成員屬性類似。
  • 靜態內部類不能使用外部類的非static成員變量或方法。
    因爲在沒有外部類的對象情況下,可以創建靜態內部類的對象,如果允許訪問外部類非static成員就會出現矛盾,因爲外部類的非static成員,必須依附於具體的對象。

靜態內部類示例

  • 靜態內部類
class Outter {

    public int num = 99;

    public static int numTwo = 100;

    /**
     * 外部類構造函數
     */
    public Outter() {

    }

    /**
     * 靜態內部類
     */
    static class Inner {
        /**
         * 靜態內部類構造函數
         */
        public Inner() {
            System.out.println(numTwo);
            // 靜態內部類無法訪問外部類沒有static修飾的成員
            System.out.println(num);
        }

        /**
         * public修飾成員
         */
        public String PUBLIC_INNER_STRING = "public_inner_string";

        /**
         * public static 修飾成員
         */
        public static String PUBLIC_STATIC_INNER_STRING = "static_inner_string";

        /**
         * private 修飾成員
         */
        private String PRIVATE_INNER_STRING = "private_inner_string";

        /**
         * private static 修飾成員
         */
        private static String PRIVATE_STATIC_INNER_STRING = "private_static_inner_string";
    }
}

在這裏插入圖片描述

  • main函數
public class Man {
    public static void main(String[] args) {
        Outter.Inner inner = new Outter.Inner();
        // 只允許訪問 public 修飾成員
        System.out.println(inner.PUBLIC_INNER_STRING);
        // 不允許訪問 public static 修飾成員
        System.out.println(inner.PUBLIC_STATIC_INNER_STRING);
        // 不允許訪問 private 修飾成員
        System.out.println(inner.PRIVATE_INNER_STRING);
        // 不允許訪問 private static 修飾成員
        System.out.println(inner.PRIVATE_STATIC_INNER_STRING);
    }
}

在這裏插入圖片描述

深入理解內部類

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

上面提到,成員內部類可以無條件的訪問外部類的成員,那到底是如何做到的呢?我們將下面的示例編譯成字節碼,通過字節碼文件來解析其中奧妙:

  • Java文件
public class Outter {

    /**
     * 聲明私有成員內部類
     */
    private Inner inner = null;

    /**
     * 外部類構造函數
     */
    public Outter() {

    }

    /**
     * 獲取成員內部類
     * @return
     */
    public Inner getInnerInstance() {
        if(inner == null) {
            inner = new Inner();
        }

        return inner;
    }

    /**
     * 成員內部類
     * protected修飾:只能在同一個包下或者繼承外部類的情況下訪問
     */
    protected class Inner {
        public Inner() {

        }
    }

}
  • 編譯,生成字節碼文件
    在這裏插入圖片描述
  • 字節碼文件
Classfile target/classes/com/frank/cp/test/Outter$Inner.class
  Last modified 2019-4-22; size 497 bytes
  MD5 checksum 893ed4fb28ba17c1675fffac1ac300c0
  Compiled from "Outter.java"
public class com.frank.cp.test.Outter$Inner
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #3.#19         // com/frank/cp/test/Outter$Inner.this$0:Lcom/frank/cp/test/Outter;
   #2 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #3 = Class              #22            // com/frank/cp/test/Outter$Inner
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               Lcom/frank/cp/test/Outter;
   #7 = Utf8               <init>
   #8 = Utf8               (Lcom/frank/cp/test/Outter;)V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Inner
  #14 = Utf8               InnerClasses
  #15 = Utf8               Lcom/frank/cp/test/Outter$Inner;
  #16 = Utf8               MethodParameters
  #17 = Utf8               SourceFile
  #18 = Utf8               Outter.java
  #19 = NameAndType        #5:#6          // this$0:Lcom/frank/cp/test/Outter;
  #20 = NameAndType        #7:#24         // "<init>":()V
  #21 = Class              #25            // com/frank/cp/test/Outter
  #22 = Utf8               com/frank/cp/test/Outter$Inner
  #23 = Utf8               java/lang/Object
  #24 = Utf8               ()V
  #25 = Utf8               com/frank/cp/test/Outter
{
  final com.frank.cp.test.Outter this$0;
    descriptor: Lcom/frank/cp/test/Outter;
    flags: ACC_FINAL, ACC_SYNTHETIC

  public com.frank.cp.test.Outter$Inner(com.frank.cp.test.Outter);
    descriptor: (Lcom/frank/cp/test/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:Lcom/frank/cp/test/Outter;
         5: aload_0
         6: invokespecial #2                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 44: 0
        line 46: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/frank/cp/test/Outter$Inner;
            0      10     1 this$0   Lcom/frank/cp/test/Outter;
    MethodParameters:
      Name                           Flags
      this$0                         final mandated
}
SourceFile: "Outter.java"
InnerClasses:
     protected #13= #3 of #21; //Inner=class com/frank/cp/test/Outter$Inner of class com/frank/cp/test/Outter

line 9至 line 35是常量池內容,我們看36行代碼:

final com.frank.cp.test.Outter this$0;

通過這行我們可以看出,編譯器默認會爲成員內部類添加一個指向外部類對象的引用,那麼它是如何賦初始值的呢?下面接着看內部類的構造器:

public com.frank.cp.test.Outter$Inner(com.frank.cp.test.Outter);

如代碼所示,雖然定義的內部類構造函數是無參的,但是編譯器默認添加了一個參數,該參數的類型爲指向外部類對象的一個引用,所以,成員內部類中的Outter this&0 指針便指向了外部類對象,因此,可以在成員內部類中隨意訪問外部類的成員。

從字節碼中也可以看出,成員內部類是依賴於外部類的,如果沒有創建外部類對象,則無法對 Outter this&0 引用進行初始化賦值,也就無法創建成員內部類對象。

使用場景和優點

使用內部類的主要原因:

  • 1.每個內部類都可以獨立繼承一個接口的實現,無論外部類是繼承了父類的子類還是實現接口的實現類,都可以使用內部類,且沒有影響。內部類使多繼承解決方案更加完整;
  • 2.方便將存在一定邏輯關係的類組裝一起,又可以對外界隱藏(詳見:靜態內部類的示例);
  • 3.方便編寫事件驅動程序(點擊事件監聽等);
  • 4.方便編寫線程代碼;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章