1. 程式人生 > >成員內部類、靜態內部類、區域性內部類和匿名內部類的理解

成員內部類、靜態內部類、區域性內部類和匿名內部類的理解

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

  一.內部類基礎

  二.深入理解內部類

  三.內部類的使用場景和好處

  四.常見的與內部類相關的筆試面試題

一.內部類基礎

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

  1.成員內部類

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




1 2 3 4 5 6 7 8 9 10 11 12 13 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成員和靜態成員)。




1 2 3 4 5 6 7 8 9 10 11 12 13 14 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);   //外部類的靜態成員 } }
}



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




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



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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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成員 } } }



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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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和包訪問兩種許可權修飾。我個人是這麼理解的,由於成員內部類看起來像是外部類的一個成員,所以可以像類的成員一樣擁有多種許可權修飾。

  2.區域性內部類

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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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修飾符的。

  3.匿名內部類

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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 } });



  這段程式碼為兩個按鈕設定監聽器,這裡面就使用了匿名內部類。這段程式碼中的:




1 2 3 4 5 6 7 8 new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub } }



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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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。一般來說,匿名內部類用於繼承其他類或是實現介面,並不需要增加額外的方法,只是對繼承方法的實現或是重寫。

  4.靜態內部類

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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test { public static void main(String[] args)  { Outter.Inner inner = new Outter.Inner(); } } class Outter { public Outter() { } static class Inner { public Inner() { } } }



  

二.深入理解內部類

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

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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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檔案得到下面資訊:




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 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 160 line 189 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引用進行初始化賦值,也就無法建立成員內部類的物件了。

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

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




1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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&#x3002;&#x9ED8;&#x8BA4;&#x60C5;&#x51B5;&#x4E0B;&#xFF0C;&#x7F16;&#x8BD1;&#x5668;&#x4F1A;&#x4E3A;&#x533F;&#x540D;&#x5185;&#x90E8;&#x7C7B;&#x548C;&#x5C40;&#x90E8;&#x5185;&#x90E8;&#x7C7B;&#x8D77;&#x540D;&#x4E3A;Outter“>1.classOutter1.class。預設情況下,編譯器會為匿名內部類和區域性內部類起名為Outterx.class(x為正整數)。

  

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

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

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

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

bipush 10

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

  下面再看一個例子:




1 2 3 4 5 6 7 8 9 10 11 12 13 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的拷貝)進行賦值初始化。

  也就說如果區域性變數的值在編譯期間就可以確定,則直接在匿