1. 程式人生 > 實用技巧 >java 內部類詳解

java 內部類詳解

內部類

什麼是內部類

  • 將一個類A定義在另一個類B裡面,裡面的那個類A就稱為內部類,B則稱為外部類。
  • 內部類可以分為四種:成員內部類、區域性內部類、匿名內部類、靜態內部類,下面我們逐一介紹這四種內部類。

成員內部類


定義在類中方法外的類。
成員內部類是最普通的內部類,它的定義為位於另一個類的內部,形如下面的形式:

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() {
            System.out.println(radius);  //外部類的private成員
            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() {
            System.out.println(radius);  //外部類的private成員
        }
    }
}

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

public class Test {
    public static void main(String[] args)  {
        //第一種方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必須通過Outter物件來建立
         
        //第二種方式:
        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 和包訪問兩種許可權修飾。我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種許可權修飾。

總結


訪問特點:

  1. 成員內部類可以直接訪問外部類的成員變數和成員方法
  2. 外部類不可以直接訪問內部類的成員變數和成員方法
  3. 外部類想要訪問內部的成員: 得建立內部類物件來訪問內部類成員

區域性內部類


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

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 修飾符的。

匿名內部類


匿名內部類應該是平時我們編寫程式碼時用得最多的,在編寫事件監聽的程式碼時使用匿名內部類不但方便,而且使程式碼更加容易維護。下面這段程式碼是一段 Android 事件監聽程式碼:

scan_bt.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
         
    }
});
 
history_bt.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
         
    }
}

就是匿名內部類的使用。程式碼中需要給按鈕設定監聽器物件,使用匿名內部類能夠在實現父類或者介面中的方法情況下同時產生一個相應的物件,但是前提是這個父類或者介面必須先存在才能這樣使用。當然像下面這種寫法也是可以的,跟上面使用匿名內部類達到效果相同。

private void setListener()
{
    scan_bt.setOnClickListener(new Listener1());       
    history_bt.setOnClickListener(new Listener2());
}
 
class Listener1 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}
 
class Listener2 implements View.OnClickListener{
    @Override
    public void onClick(View v) {
    // TODO Auto-generated method stub
             
    }
}

這種寫法雖然能達到一樣的效果,但是既冗長又難以維護,所以一般使用匿名內部類的方法來編寫事件監聽程式碼。同樣的,匿名內部類也是不能有訪問修飾符和 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() {
             
        }
    }
}

深入理解內部類


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

在此之前,我們已經討論過了成員內部類可以無條件訪問外部類的成員,那具體究竟是如何實現的呢?下面通過反編譯位元組碼檔案看看究竟。事實上,編譯器在進行編譯的時候,會將成員內部類單獨編譯成一個位元組碼檔案,下面是 Outter.java 的程式碼:

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

編譯之後,出現了兩個位元組碼檔案:

反編譯 Outter$Inner.class 檔案得到下面資訊:

E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
  SourceFile: "Outter.java"
  InnerClass:
   #24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
  minor version: 0
  major version: 50
  Constant pool:
const #1 = class        #2;     //  com/cxh/test2/Outter$Inner
const #2 = Asciz        com/cxh/test2/Outter$Inner;
const #3 = class        #4;     //  java/lang/Object
const #4 = Asciz        java/lang/Object;
const #5 = Asciz        this$0;
const #6 = Asciz        Lcom/cxh/test2/Outter;;
const #7 = Asciz        <init>;
const #8 = Asciz        (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz        Code;
const #10 = Field       #1.#11; //  com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;//  this$0:Lcom/cxh/test2/Outter;
const #12 = Method      #3.#13; //  java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;//  "<init>":()V
const #14 = Asciz       ()V;
const #15 = Asciz       LineNumberTable;
const #16 = Asciz       LocalVariableTable;
const #17 = Asciz       this;
const #18 = Asciz       Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz       SourceFile;
const #20 = Asciz       Outter.java;
const #21 = Asciz       InnerClasses;
const #22 = class       #23;    //  com/cxh/test2/Outter
const #23 = Asciz       com/cxh/test2/Outter;
const #24 = Asciz       Inner;
 
{
final com.cxh.test2.Outter this$0;
 
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
  Code:
   Stack=2, Locals=2, Args_size=2
   0:   aload_0
   1:   aload_1
   2:   putfield        #10; //Field this$0:Lcom/cxh/test2/Outter;
   5:   aload_0
   6:   invokespecial   #12; //Method java/lang/Object."<init>":()V
   9:   return
  LineNumberTable:
   line 16: 0
   line 18: 9
 
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      10      0    this       Lcom/cxh/test2/Outter$Inner;
 
 
}

第11行到35行是常量池的內容,下面逐一第38行的內容:

final com.cxh.test2.Outter this$0;

這行是一個指向外部類物件的指標,看到這裡想必大家豁然開朗了。也就是說編譯器會預設為成員內部類添加了一個指向外部類物件的引用,那麼這個引用是如何賦初值的呢?下面接著看內部類的構造器:

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

從這裡可以看出,雖然我們在定義的內部類的構造器是無參構造器,編譯器還是會預設新增一個引數,該引數的型別為指向外部類物件的一個引用,所以成員內部類中的 Outter this&0 指標便指向了外部類物件,因此可以在成員內部類中隨意訪問外部類的成員。從這裡也間接說明了成員內部類是依賴於外部類的,如果沒有建立外部類的物件,則無法對 Outter this&0 引用進行初始化賦值,也就無法建立成員內部類的物件了。

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

想必這個問題也曾經困擾過很多人,在討論這個問題之前,先看下面這段程式碼:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

這段程式碼會被編譯成兩個class檔案:Test.class和Test1.class。預設情況下,編譯器會為匿名內部類和區域性內部類起名為Outterx.class(x為正整數)。

根據上圖可知,test 方法中的匿名內部類的名字被起為 Test$1。

上段程式碼中,如果把變數 a 和 b 前面的任一個 final 去掉,這段程式碼都編譯不過。我們先考慮這樣一個問題:

當 test 方法執行完畢之後,變數a的生命週期就結束了,而此時 Thread 物件的生命週期很可能還沒有結束,那麼在 Thread 的 run 方法中繼續訪問變數 a 就變成不可能了,但是又要實現這樣的效果,怎麼辦呢?Java 採用了複製的手段來解決這個問題。將這段程式碼的位元組碼反編譯可以得到下面的內容:

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

bipush 10

這條指令表示將運算元10壓棧,表示使用的是一個本地區域性變數。這個過程是在編譯期間由編譯器預設進行,如果這個變數的值在編譯期間可以確定,則編譯器預設會在匿名內部類(區域性內部類)的常量池中新增一個內容相等的字面量或直接將相應的位元組碼嵌入到執行位元組碼中。這樣一來,匿名內部類使用的變數是另一個區域性變數,只不過值和方法中區域性變數的值相等,因此和方法中的區域性變數完全獨立開。
下面再看一個例子:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int a) {
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

反編譯得到:

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

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

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

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

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

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

從前面可以知道,靜態內部類是不依賴於外部類的,也就說可以在不建立外部類物件的情況下建立內部類的物件。另外,靜態內部類是不持有指向外部類物件的引用的,這個讀者可以自己嘗試反編譯 class 檔案看一下就知道了,是沒有 Outter this&0 引用的。

內部類的使用場景和好處

為什麼在 Java 中需要內部類?總結一下主要有以下四點:

1.每個內部類都能獨立的繼承一個介面的實現,所以無論外部類是否已經繼承了某個(介面的)實現,對於內部類都沒有影響。內部類使得多繼承的解決方案變得完整。

2.方便將存在一定邏輯關係的類組織在一起,又可以對外界隱藏。

3.方便編寫事件驅動程式。

4.方便編寫執行緒程式碼。

個人覺得第一點是最重要的原因之一,內部類的存在使得Java的多繼承機制變得更加完善。

原文出處:https://www.cnblogs.com/dolphin0520/p/3811445.html