Java基礎-內部類詳解

Yuicon發表於2018-08-26

我的部落格 轉載請註明原創出處。

內部類(inner class)是定義在另一個類內部的類。之所以定義在內部是因為內部類有一些普通類沒有的“特權”,可以方便實現一些需求。

內部類

先來看一個簡單的例子:

public class Apple {

    private int size = 16;

    private class Book {
        public void print() {
            System.out.println(size);
        }
    }

}
複製程式碼

Book類就是定義在Apple類中的一個內部類,Book類引用了Apple類的私有域size卻沒有報錯,這就是上文提到的特權了,內部類可以引用外圍類的所有域和方法包括私有的。那麼為什麼內部類可以做到這樣神奇的事情呢?原來是編譯器在背後偷偷乾的好事!

把上文的例子編譯後可以看到編譯器會額外生成一個Apple$Book.class檔案:

class Apple$Book {
    private Apple$Book(Apple var1) {
        this.this$0 = var1;
    }

    public void print() {
        System.out.println(Apple.access$000(this.this$0));
    }
}
複製程式碼

可以看到這個類的名稱是用外圍類名稱加內部類名稱用$符號分割,而且編譯器在內部類的建構函式裡自動新增了一個外圍類的引數,這樣內部類就能引用到外圍類的域和引數了。

不過這樣還有一個問題,我們完全可以按普通的方式自己寫一個構建方式來接收Apple類而不用內部類的方式,不過這樣的類卻無法引用Apple類的私有域和私有方法。

眼尖的同學可能已經發現奧祕了,Apple.access$000(this.this$0)這一條語句就是關鍵了。內部類在引用外圍類的私有域和方法時編譯器會在外圍類內部生成一個靜態方法access$XXX,這個方法會返回外圍類的私有域或呼叫私有方法,方法的第一個引數是外圍類的引用。

不過這樣就有了安全風險,任何人都可以通過呼叫Apple.access$000方法很容易地讀取到私有域size。當然,access$000不是Java的合法方法名。但熟悉類檔案結構的黑客可以使用十六進位制編輯器輕鬆地建立一個用虛擬機器指令呼叫那個方法的類檔案。由於隱祕地訪問方法需要擁有包可見性,所以攻擊程式碼需要與被攻擊類放在同一個包中。

特殊的語法

內部類有一些特殊的語法,比如獲取傳入的外圍類引用的語法是OuterClass.this,外圍類的類名加上this關鍵字。還有明確的使用內部類的構建函式outerObject.new InnerClass {construction parameters)。在內部類中宣告的靜態域必須是不可變的,即必須用final修飾符修飾,且不能有靜態方法。例子:

public class Apple {

    private int size = 16;

    private class Book {
        public void print() {
            System.out.println(Apple.this.size);
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Apple.Book book = apple.new Book();
    }

}
複製程式碼

區域性內部類

內部類也可以在一個方法內宣告,這樣定義的內部類就是區域性內部類。區域性內部類和內部類的區別在於區域性內部類的作用域侷限於定義它的方法塊內,除了這個方法內部區域性內部類都是不可見的。

public class Apple {

    private int size = 16;

    private void print() {
        class Book {
            public void print() {
                System.out.println(size);
            }
        }
        Book book = new Book();
        book.print();
    }

}
複製程式碼

匿名內部類

顧名思義,匿名內部類是一種沒有類名的類。因為有時候我們只需要有一個一次性使用的類的物件,匿名內部類可以方便我們實現。通常的語法格式為:

SuperType superType = new SuperType(construction parameters) {
    inner class methods and data
}
複製程式碼

如果SuperType是一個介面,那麼就需要在大括號裡實現介面定義的抽象方法。如果SuperType是一個類,可以在大括號裡擴充套件這個類。因為匿名內部類沒有類名,所以是不能定義構建函式的。在Java8以後,使用lambda表示式會比匿名內部類更加方便。

雙括號初始化

利用匿名內部類的特殊語法的特殊初始化技巧,比如初始化一個陣列:

List<String> arrayList = new ArrayList<String>() {{
    add("test");
    add("test2");
}};
複製程式碼

不過就這個例子來說這樣更好:List<String> arrayList = Arrays.asList("test", "test2");

靜態內部類

上文說到內部類都會有一個外圍類的引用,不過有時我們只是想把類放在另一個類內部並不需要引用它,這時就可以用到靜態內部類。例子:

public class Apple {

    private int size;

    private int price;

    public Apple(int size, int price) {
        this.size = size;
        this.price = price;
    }

    public static void main(String[] args) {
        Apple apple = AppleBuilder.builder().setPrice(20).setSize(16).build();
    }

    static class AppleBuilder {

        private int size;

        private int price;

        static AppleBuilder builder() {
            return new AppleBuilder();
        }

        Apple build() {
            return new Apple(size, price);
        }

        AppleBuilder setSize(int size) {
            this.size = size;
            return this;
        }

        AppleBuilder setPrice(int price) {
            this.price = price;
            return this;
        }

    }

}
複製程式碼

後記

一週一篇的第八篇了,接下來再複習一下併發相關的內容就準備去看看JVM相關的內容了。知識學是學不完的,只希望自己能堅持學到老,不要待在舒適區變成一個曾經討厭的老頑固。這次同樣是參考《Java核心技術 卷1》,這可真是一本好書,建議Java新手都去看看。

相關文章