1. 程式人生 > >Java內部類、靜態類、區域性類詳解

Java內部類、靜態類、區域性類詳解

先看一個例子:

class TalkingClock
{
    private int interval;
    private boolean beep;
    public TalkingClock(int interval,boolean beep) {
        this.interval = interval;
        this.beep = beep;
    }

例子來自《Java核心技術》,這是一個“會叫的時鐘”類,功能是每隔一段時間interval,在螢幕上列印當前時間,並且根據beep變數是否為真,決定是否發出beep的響聲。可以看到在建構函式中設定interval和beep屬性後,呼叫start()方法就可以運行了。

    public void start() {
        ActionListener listener = new TimePrinter();
        Timer timer = new Timer(interval, listener);
        timer.start();
    }
    public class TimePrinter implements ActionListener
    {
        @Override
        public void actionPerformed(ActionEvent e) {
            Date now = new
Date(); System.out.println("Now the time is : "+now); if (beep) { Toolkit.getDefaultToolkit().beep(); } } }

這裡的TimePrinter是一個事件監聽器,同時也是TalkingClock的內部類。

內部類的訪問控制

我們注意到一個細節:

if (beep) {
                Toolkit.getDefaultToolkit().beep
(); }

這裡引用了外部類的私有屬性beep,說明內部類可以隨意獲取所在的外部類的各種public、protected、private屬性、方法。這是怎麼做到的呢?
實質上,內部類只是編譯器提供的一個“假象”。在編譯後的class檔案中,內部類會被移出來,生成一個新的和外部類平行的類,名字為 外部類$內部類 ,如上面程式碼生成類名為 TalkingClock$TimePrinter 。
用javap 命令把這個類打印出來看一下

這裡寫圖片描述

com.Joey是所在包名。注意紅色圈出來的地方,這個奇怪的名字this$0是編譯器自動生成的,指向外部類TalkingClock的一個引用。在建構函式中,給這個引用賦值,所以內部類就能夠通過這個引用來獲取到外部類的屬性。
但是,僅有這個引用是不夠的,因為這時候內部類已經和外部類處於平級的位置,所以按規定是無法訪問到外部類private的屬性的,怎麼解決這個問題呢?

我們用javap把外部類也打印出來看一下
這裡寫圖片描述

同樣注意到紅線部分,編譯器新增了一個access$0的方法,傳入引數為TalkingClock。正是這個靜態方法的內部,把beep私有屬性返回了,所以我們的語句

if(beep)

實際上編譯成

if(access$0(outer))

即通過access$0方法來獲取外部類的私有屬性

於是我們可以知道,當內部類需要訪問外部類 public 或 protected 成員時,在內部類中由編譯器生成 final 型別的 this$0 欄位,通過建構函式把外部類引用賦值給 this$0 ;當內部類需要訪問外部類 private 成員(包括私有屬性和方法),在外部類中由編譯器生成 static 型別的訪問器方法 access$0,通過該方法來間接訪問或修改外部類的私有成員。

靜態內部類

在前面的例子中,我們發現內部類有很大的特權,可以隨意的訪問外部類中的成員變數與成員方法。有時候我們希望限制這種特權,有一種方法就是把內部類設定為靜態的,即新增static修飾符。

在C#中,static可以修飾任意一個類,稱為靜態類。靜態類只能有靜態成員,不能被例項化,通常用於作為全域性共享的工具類,提供共享的屬性和方法,以簡化訪問操作。

而java不一樣,java的static只能用於修飾內部類,不能作用於其他的類。而java的靜態內部類,限制只能夠引用外部類中的靜態成員方法或者成員變數,對於那些非靜態的成員變數與成員方法,由於靜態內部類中沒有對外部類的引用,所以在靜態內部類中是無法訪問的。這就是靜態內部類的最大使用限制。

靜態內部類當然不僅僅只有這個優點。

我們知道,非靜態內部類中不能存在靜態變數,如

public class OuterClass
{
    class InnerClass
    {
        static int a = 0;
    }
}

會發生編譯錯誤。

從語義的角度來說,內部類是依附外部類而存在的,儘管實際上編譯成為兩個平級的類,但是外界是通過一個外部類的例項來引用內部類的。如

OuterClass outer = new OuterClass();
OuterClass InnerClass = outer.new InnerClass();

而靜態變數的特點屬於類而不屬於某一個例項,所以從這個角度來說,非靜態內部類的如果存在一個靜態變數,則說明這個變數可以通過 OuterClass.InnerClass.靜態變數 這樣的語句來直接訪問,這就和內部類的定義矛盾了。

但是可以存在final修飾的靜態常量,如

public class OuterClass
{
    class InnerClass
    {
        static final int a = 0;
    }
}

不會編譯出錯,這是為什麼呢?

從編譯器的角度,可以很容易的找到答案。static final 修飾的是 靜態常量 也就是在編譯期間就可以確定的常量。在編譯期間,編譯器有一個很重要的優化手段就是常量優化。也就是說在編譯期間能確定的常量,會放在類檔案的常量區。在初始化類的時候,從常量區中取出並賦值給 static final 變數。

一旦把內部類設定成靜態的,那麼就可以在內部類中定義靜態變量了,此時意味著可以在外界通過 OuterClass.InnerClass.靜態變數來訪問 ,也意味著這個靜態類不再依附某一個特定的外部類例項,可以這樣來建立:

OuterClass.InnerClass aClass = new InnerClass();

跟一般的類不同,靜態內部類生成的例項只有一個,也就是說就算生成多次,也會是多個引用執行同一個例項,如:

class OuterClass
{
    static class InnerClass
    {
        static  int n= 1;
    }
    public static void main(String[] args) {
        OuterClass.InnerClass aClass = new InnerClass();
        OuterClass.InnerClass bClass = new InnerClass();
        bClass.n = 2;
        System.out.println(aClass.n);
    }
}

結果輸出 2,證明兩個引用指向同一個例項。這個特性使得當使用多個外部類的物件可以共享同一個內部類的物件。

總的來說,靜態內部類犧牲了普通內部類訪問外部類私有屬性的特權,換來更高的自由度和只有一個例項(靜態的本意就是全域性共享且唯一)的能力。靜態內部類和對應的外部類僅有名義上的從屬關係,沒有共生死的關係。

區域性內部類

所謂區域性內部類,就是把內部類用 { } 封裝起來。很多資料書上給出的解釋是定義在方法體內的類(當然同時也是內部類),叫做區域性內部類,其實不盡然。在類的初始程式碼塊 { } 以及靜態程式碼塊 static { } 中定義的內部類,其表現和定義在方法中一樣。由於在類載入期間,static { } 中的程式碼和靜態屬性賦值程式碼會合並生成一個 < clinit >()方法,初始程式碼塊{ }會合並進入例項構造器,所以從這個角度上來說,把定義在方法體內的類稱為區域性內部類不無道理。

由於被限定在方法體內,區域性內部類相比普通內部類有了更大的侷限性。就可見性而言,只有方法體內才能知道它的存在,出了方法體,哪怕是在外部類中都看不到它,因此區域性內部類沒有public等修飾符,對於外界而言完全是隱藏起來的。

相比普通內部類,區域性內部類還有一個優勢,就是訪問其所在方法的區域性變數。但是有個限制,訪問的區域性變數必須顯示或隱式地設定成 final 型別。如

class OuterClass 
{
    {
        int a = 0;
        class InnerClass {
            public InnerClass() {
                System.out.println(a);
            }
        }
        new InnerClass();       
    }

這段程式碼中,InnerClass定義在初始化程式碼塊中,建構函式試圖列印一個區域性變數a的值。當然就這段程式碼而言是沒有問題的,即便沒有顯示地設定a 為 final 型別。但是當試圖修改 a 的值時,如

class OuterClass 
{
    {
        int a = 0;
        class InnerClass {
            public InnerClass() {
                System.out.println(a);
            }
        }
        a++;
        new InnerClass();       
    }

此時會報錯:Local variable a defined in an enclosing scope must be final or effectively final

也就是說此時 a 必須為 final型別,也就不能被修改

為什麼會有這樣奇怪的設定呢?其實這是由於生命週期不同而做出的妥協。在方法體中,區域性變數的生命週期是隨著方法的結束而結束的,但是區域性類不會隨方法體結束而成為垃圾。一旦還有對區域性類的引用,那麼它將不會被垃圾收集器回收。所以會出現這樣一種情況:區域性類試圖訪問一個已經隨著方法結束而滅亡的區域性變數,這種行為是不允許的。

然而區域性類可以訪問外圍方法的區域性變數這個要求卻是很合理的。為了解決這個矛盾,Java語言設計人員採取了複製變數的方法,即在區域性類中由編譯器生成一個屬性,把區域性變數賦值給該屬性,從而把所有對區域性變數的引用轉化為對區域性類的屬性的引用。

我們對上面的程式碼class檔案反編譯一下看看:

這裡寫圖片描述

不知道為什麼,用javap檢視的區域性類中沒有出現複製生成的屬性,但是我們可以從構造器中看到,傳入的引數一個是對外部類的引用,一個是外圍方法區域性變數的值,所以Java的確是通過複製值的方式實現的。

通過複製的方式可以實現訪問區域性變數,但是存在一個很大的問題——變數一致性問題。試想由於區域性類的值是在建構函式中傳入的,在賦值之後如果區域性變數改變了,會導致複製後的值和原來值不一致,這樣就做不到“訪問外圍方法區域性變數”的初衷了。所以Java設計人員採用一種比較簡單粗暴的做法,強制訪問的區域性變數必須是 final 型別,這樣由於值不可修改,也就避免了一致性問題。

然而這樣的一刀切的做法有時候會帶來不便。為什麼不能在區域性類中再設定get和set方法,實時同步複製的值和區域性變數的值呢?我們不得而知,當然在絕大多數的情況下設定成 final 型別已經足夠了,如果真的有需求改變區域性變數的值,可以通過一些變通的辦法,如

class OuterClass 
{
    {
        final int[] a = {0};
        class InnerClass {
            public InnerClass() {
                a[0]++;
                System.out.println(a[0]);
            }
        }
        new InnerClass();       
        System.out.println(a[0]);
    }

我們指定一個 final 型別的陣列,陣列中只有一個元素,這樣就可以繞開Java的錯誤提示,因為 final 作用於物件時指的是這個引用不可修改,而引用的變數可以修改,所以我們在區域性類的內部就可以對 a[0] 元素進行任意的訪問和修改,結果會同步到外圍方法的區域性變數中。這個例子輸出結果是兩個 1

使用這樣的技巧時候要特別小心,因為注意到區域性類的值是在物件生成時通過建構函式傳入的。所以一旦物件生成,區域性變數的值就反映不到區域性類中去了,如

class OuterClass 
{
    {
        final int[] a = {0};
        class InnerClass {
            public InnerClass() {
                System.out.println(a[0]);
            }
        }
        a[0]++;
        new InnerClass();   
        System.out.println(a[0]);
    }

這樣區域性類的值為 1 ,如果把

a[0]++;
new InnerClass();

改為

new InnerClass();
a[0]++;

則區域性類的值仍然為 0