Java語法糖4:內部類

五月的倉頡發表於2015-10-14

內部類

最後一個語法糖,講講內部類,內部類指的就是在一個類的內部再定義一個類。

內部類之所以也是語法糖,是因為它僅僅是一個編譯時的概念,outer.java裡面定義了一個內部類inner,一旦編譯成功,就會生成兩個完全不同的.class檔案了,分別是outer.class和outer$inner.class。所以內部類的名字完全可以和它的外部類名字相同

內部類分為四種:成員內部類、區域性內部類、匿名內部類、靜態內部類。先逐一瞭解下,再看下使用內部類有什麼好處。

 

成員內部類

成員內部類是最常見的內部類,就是在外部類的基礎上按照一般定義類的方式定義類罷了,看一個例子:

public class Outer
{
    private int i;
    
    public Outer(int i)
    {
        this.i = i;
    }
    
    public void privateInnerGetI()
    {
        new PrivateInner().printI();
    }
    
    private class PrivateInner
    {
        public void printI()
        {
            System.out.println(i);
        }
    }
    
    public class PublicInner
    {
        private int i = 2;
        
        public void printI()
        {
            System.out.println(i);
        }
    }
}

主函式為:

public static void main(String[] args)
{
    Outer outer = new Outer(0);
    outer.privateInnerGetI();
    Outer.PublicInner publicInner = outer.new PublicInner();
    publicInner.printI();
}

執行結果為:

0
2

通過這個例子總結幾點:

1、成員內部類是依附其外部類而存在的,如果要產生一個成員內部類,比如有一個其外部類的例項

2、成員內部類中沒有定義靜態方法,不是例子不想寫,而是成員內部類中不可以定義靜態方法

3、成員內部類可以宣告為private的,宣告為private的成員內部類對外不可見,外部不能呼叫私有成員內部類的public方法

4、成員內部類可以宣告為public的,宣告為public的成員內部類對外可見,外部也可以呼叫共有成員內部類的public方法

5、成員內部類可以訪問其外部類的私有屬性,如果成員內部類的屬性和其外部類的屬性重名,則以成員內部類的屬性值為準

 

區域性內部類

區域性內部類是定義在一個方法或者特定作用域裡面的類,看一下區域性內部類的使用:

public static void main(String[] args)
{
    final int i = 0;
    class A
    {
        public void print()
            {
            System.out.println("AAA, i = " + i);
        }
    }
    
    A a = new A();
    a.print();
}

注意一下區域性內部類沒有訪問修飾符,另外區域性內部類要訪問外部的變數或者物件,該變數或物件的引用必須是用final修飾的

 

匿名內部類

這個應該是用得最多的,因為方便,在多執行緒模組中的程式碼示例中大量使用了匿名內部類,隨便找一段:

public static void main(String[] args) throws InterruptedException
{
    final ThreadDomain44 td = new ThreadDomain44();
    Runnable runnable = new Runnable()
    {
        public void run()
        {
            td.testMethod();
        }
    };
    Thread[] threads = new Thread[10];
    for (int i = 0; i < 10; i++)
        threads[i] = new Thread(runnable);
    for (int i = 0; i < 10; i++)
        threads[i].start();
    Thread.sleep(2000);
    System.out.println("有" + td.lock.getQueueLength()  "個執行緒正在等待!");
}

匿名內部類是唯一沒有構造器的類,其使用範圍很有限,一般都用於繼承抽象類或實現介面(注意只能繼承抽象類,不能繼承普通類),匿名內部類Java自動為之起名為XXX$1.classs。另外,和區域性內部類一樣,td必須是用final修飾的。

 

靜態內部類

用static修飾的內部類就是靜態內部類,看下例子:

public class Outer
{
    private static final int i = 1;public static class staticInner
    {
        public void notStaticPrint()
        {
            System.out.println("Outer.staticInner.notStaticPrint(), i = " + i);
        }
        
        public static void staticPrint()
        {
            System.out.println("Outer.staticInner.staticPrint()");
        }
    }
}
public static void main(String[] args)
{
    Outer.staticInner os = new Outer.staticInner();
    os.notStaticPrint();
    Outer.staticInner.staticPrint();
}

執行結果為:

Outer.staticInner.notStaticPrint(), i = 1
Outer.staticInner.staticPrint()

通過這個例子總結幾點:

1、靜態內部類中可以有靜態方法,也可以有非靜態方法

2、靜態內部類只能訪問其外部類的靜態成員與靜態方法

3、和普通的類一樣,要訪問靜態內部類的靜態方法,可以直接"."出來不需要一個類例項;要訪問靜態內部類的非靜態方法,必須拿到一個靜態內部類的例項物件

4、注意一下例項化成員內部類和例項化靜態內部類這兩種不同的內部類時寫法上的差別

(1)成員內部類:外部類.內部類 XXX = 外部類.new 內部類();

(2)靜態內部類:外部類.內部類 XXX = new 外部類.內部類();

 

為什麼成員內部類可以訪問外部類成員

用"javap"命令反編譯一下第一個例子的內部類privateInner:

看一下這個內部類裡的常量池中有哪些符號引用就知道了:

Constant pool:
   #1 = Class              #2             //  com/xrq/test29/Outer$PrivateInner
   #2 = Utf8               com/xrq/test29/Outer$PrivateInner
   #3 = Class              #4             //  java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               this$0
   #6 = Utf8               Lcom/xrq/test29/Outer;
   #7 = Utf8               <init>
   #8 = Utf8               (Lcom/xrq/test29/Outer;)V
   #9 = Utf8               Code
  #10 = Fieldref           #1.#11         //  com/xrq/test29/Outer$PrivateInner.
this$0:Lcom/xrq/test29/Outer;
  #11 = NameAndType        #5:#6          //  this$0:Lcom/xrq/test29/Outer;
  #12 = Methodref          #3.#13         //  java/lang/Object."<init>":()V
  #13 = NameAndType        #7:#14         //  "<init>":()V
  #14 = Utf8               ()V
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lcom/xrq/test29/Outer$PrivateInner;
  #19 = Utf8               printI
  #20 = Fieldref           #21.#23        //  java/lang/System.out:Ljava/io/Prin
tStream;
  #21 = Class              #22            //  java/lang/System
  #22 = Utf8               java/lang/System
  #23 = NameAndType        #24:#25        //  out:Ljava/io/PrintStream;
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Methodref          #27.#29        //  com/xrq/test29/Outer.access$0:(Lco
m/xrq/test29/Outer;)I
  #27 = Class              #28            //  com/xrq/test29/Outer
  #28 = Utf8               com/xrq/test29/Outer
  #29 = NameAndType        #30:#31        //  access$0:(Lcom/xrq/test29/Outer;)I

  #30 = Utf8               access$0
  #31 = Utf8               (Lcom/xrq/test29/Outer;)I
  #32 = Methodref          #33.#35        //  java/io/PrintStream.println:(I)V
  #33 = Class              #34            //  java/io/PrintStream
  #34 = Utf8               java/io/PrintStream
  #35 = NameAndType        #36:#37        //  println:(I)V
  #36 = Utf8               println
  #37 = Utf8               (I)V
  #38 = Utf8               (Lcom/xrq/test29/Outer;Lcom/xrq/test29/Outer$PrivateI
nner;)V
  #39 = Methodref          #1.#40         //  com/xrq/test29/Outer$PrivateInner.
"<init>":(Lcom/xrq/test29/Outer;)V
  #40 = NameAndType        #7:#8          //  "<init>":(Lcom/xrq/test29/Outer;)V

  #41 = Utf8               SourceFile
  #42 = Utf8               Outer.java
  #43 = Utf8               InnerClasses
  #44 = Utf8               PrivateInner

關鍵地方是兩個:

1、第5行和第6行,Outer\$PrivateInner裡面有一個this\$0,它是一個Lcom/xrq/test29/outer,開頭的L表示複合物件。這表示內部類中有一個其外部類的引用

2、第7行和第8行,表示this$0這個引用通過建構函式賦值

順便說一句,靜態內部類並不持有其外部類的引用

 

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

我是這麼理解這個問題的。

開頭就說了,內部類是一種語法糖,所謂語法糖,就是Java編譯器在編譯期間做的手腳,既然是在編譯期間做的手腳,那麼如何知道執行方法期間才確定的某個區域性變數的值是多少?先理清楚兩點:

  • 匿名內部類是唯一沒有構造器的類
  • 區域性內部類有構造器,通過構造器把外部的變數傳入區域性內部類再使用是完全可以的

那萬一區域性內部類中沒有定義構造器傳入區域性變數怎麼辦呢?這時候Java想了一個辦法,把區域性變數修飾為final就好了,被final修飾的變數相當於是一個常量,編譯時就可以確定並放入常量池。這樣即使匿名內部類沒有構造器、區域性內部類沒有定義有參構造器,也無所謂,反正要用到的變數編譯時候就已經確定了,到時候去常量池裡面拿一下就好了。

既然上面說到了"去常量池裡面拿一下就好了",那麼把區域性內部類、匿名內部類裡面要用到的區域性變數設定為static的也是可以的(不過static不可以修飾區域性變數,可以放在方法外),可以自己試一下

 

使用內部類的好處

最後來總結一下使用內部類的好處:

1、Java允許實現多個介面,但不允許繼承多個類,使用成員內部類可以解決Java不允許繼承多個類的問題。在一個類的內部寫一個成員內部類,可以讓這個成員內部類繼承某個原有的類,這個成員內部類又可以直接訪問其外部類中的所有屬性與方法,是不是相當於多繼承了呢?

2、成員內部類可以直接訪問其外部類的private屬性,而新起一個外部類則必須通過setter/getter訪問類的private屬性

3、有些類明明知道程式中除了某個固定地方都不會再有別的地方用這個類了,為這個只用一次的類定義一個外部類顯然沒必要,所以可以定義一個區域性內部類或者成員內部類,寫一段程式碼用用就好了

4、內部類某種程度上來說有效地對外隱藏了自己,比如我們常用的開發工具Eclipse、MyEclipse,看程式碼一般用的都是Packge這個導航器,Package下只有.java檔案,我們是看不到定義的內部類的.java檔案的

5、使用內部類可以讓類與類之間的邏輯上的聯絡更加緊密

相關文章