java從入門到放棄--[1.7]內部類

內部類

內部類(inner class),顧名思義就是定義在另一個類中的類。如下所示:

public class OuterClass {
    public class InnerClass {
    }
}

相對於的,包含了其他類的類,我們稱爲外部類或外圍類(OuterClass)。

爲什麼需要使用內部類?它主要有如下一些作用和特性:

  • 內部類和包含它的外圍類的方法在同一個層次,因此也可以稱爲成員內部類,它可以使用 pubic、protected、private 修飾符。

  • 成員內部類中不能存在任何 static 的變量和方法,但可以定義常量。

  • 內部類的方法可以訪問該類定義所在的作用域中的數據,包括私有數據。

  • 內部類對同一個包中的其他類不可見。

  • 當要定義一個回調函數而不想編寫大量代碼時,可以使用匿名內部類(anonymous inner class)。

內部類之所以可以訪問包含它的外圍類的屬性和方法,實際上是因爲每一個內部類對象都持有一個指向包含它的外圍類對象的引用。這個外圍類的引用是在內部類的構造器中設置。編譯器修改了所有內部類的構造器,添加了一個外圍類引用的參數,和之前對類實例方法調用時的 this 隱藏參數類似。

在內部類中使用外圍類引用的正規語法:OuterClass.this 表示外圍類引用;如果未使用此前綴時,默認訪問內部類定義的屬性和方法,內部類不存在相同的屬性和方法時則訪問外圍類的屬性和方法。如下代碼所示:

public class OuterClass {
    private Integer index;
    private String name;
​
    public void setIndex(Integer index) {
        this.index = index;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public void printName() {
        System.out.println(name);
    }
​
    public class InnerClass {
        private String name;
​
        public void setName(String name) {
            this.name = name;
        }
​
        public void printName() {
            System.out.println(name);
            System.out.println("我的外圍類Name=" + OuterClass.this.name);
            System.out.println("我的外圍類Index=" + index);
        }
    }
​
    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
        oc.setIndex(1);
        oc.setName("我是外圍類");
        oc.printName();
​
        OuterClass.InnerClass ic = oc.new InnerClass();
        ic.setName("我是內部類");
        ic.printName();
    }
}

內部類的創建

定義了內部類後,我們怎麼來創建一個內部類的實例?通常有兩種方式:

  1. 在包含它的作用域範圍內,直接使用 new 操作符來構造一個實例,和普通類的構造方式一樣。

  2. 在包含它的作用域範圍外,使用 外圍類對象.new 變量名 內部類類名() 語法來創建;在作用域範圍外,我們這樣來引用內部類:OuterClass.InnerClass

public class OuterClass {
    public class InnerClass {
    }
​
    public InnerClass createInnerClass() {
        return new InnerClass();
    }
​
    public static void main(String[] args) {
        OuterClass oc = new OuterClass();
​
        OuterClass.InnerClass ic1 = oc.createInnerClass();
        OuterClass.InnerClass ic2 = oc.new InnerClass();
    }
}

局部內部類

除了在類裏定義其他類,我們還可以在方法內,或者代碼塊內等地方來定義一個類。這些地方定義的類我們稱爲局部內部類。局部內部類有如下一些特性:

  • 不能使用 public、protected 或 private 訪問修飾符進行聲明,它的作用域被限定在聲明這個局部類的塊中。

  • 和其他內部類相比,局部內部類除了可以訪問包含它的外圍類外,還可以訪問作用域範圍內的局部變量。但是,這些局部變量必須被聲明爲 final。

  • Java 8 版本開始,局部內部類訪問方法中的局部變量,可以不聲明爲 final,這是因爲 Java 8 增加了 effectively final 特性,它會對相關局部變量自動聲明爲 final:http://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html

下邊我們使用局部內部類來改寫一下前邊示例中內部 InnerClass 的 printName 方法:

public void printName() {
    String method = "printName";
​
    class LocalInnerClass {
        public void println() {
            System.out.println(name);
            System.out.println("我的外圍類Name=" + OuterClass.this.name);
            System.out.println("我的外圍類Index=" + index);
            System.out.println("方法名=" + method);
        }
    }
​
    new LocalInnerClass().println();
}

匿名內部類

對於局部內部類,如果我們只創建這個類的一個實例對象,那麼我們就沒必要給它命名了。這種沒有名字的內部類,我們稱爲匿名內部類(anonymous inner class)。

下邊我們再來改寫一下上邊的 printName 方法:

public interface PrintInterface {
    void println();
}
public void printName() {
    String method = "printName";
​
    new PrintInterface() {
        @Override
        public void println() {
            System.out.println(name);
            System.out.println("我的外圍類Name=" + OuterClass.this.name);
            System.out.println("我的外圍類Index=" + index);
            System.out.println("方法名=" + method);
        }
    }.println();
}

上述匿名內部類的語法格式爲:

new SuperType(construction parameters) {
    inner class methods and data;
}

注意:由於構造器的名字必須和類名相同,而匿名類沒有類名,所以匿名類不能有構造器,因此上述語法是將構造器參數傳遞給父類構造器;而當匿名內部類實現的是接口時就不能有任何構造參數。

雙括號初始化(double brace initialization):我們先來回憶一下什麼是初始化代碼塊?那麼對於匿名類中是否也可以有初始化代碼塊?答案是肯定的,比如我要創建一個 List,然後給它添加一些初始值,下邊兩段代碼是等效的:

List<Integer> numbs = new ArrayList();
numbs.add(1);
numbs.add(2);
numbs.add(3);
numbs.add(4);
numbs.add(5);
System.out.println(numbs);
​
List<Integer> numbs2 = new ArrayList() {{
    add(1);
    add(2);
    add(3);
    add(4);
    add(5);
}};
System.out.println(numbs2);

靜態內部類

有時我們僅僅只需要把一個類隱藏在另一個類中,而並不需要內部類引用外圍類對象,這時只需要將內部類聲明爲 static 即可。

使用 static 聲明的內部類稱爲靜態內部類或者嵌套類。

例如:我們有一個方法,它返回指定的一組數據中的最大值和最小值。代碼我們可以這樣寫,如下所示:

public class CalcMinMax {
    public int[] calc(int... numbs) {
        int min = 0;
        int max = 0;

        for (int numb : numbs) {
            if (min > numb) {
                min = numb;
            }
            if (max < numb) {
                max = numb;
            }
        }

        return new int[]{min, max};
    }
}

int[] mm = new CalcMinMax().calc(1, 4, 9, 3, 0);
System.out.println("最小值:" + mm[0]);
System.out.println("最大值:" + mm[1]);

上述代碼中,我們使用了一個二維數組來表示返回值的最小值和最大值,在沒有說明的情況下,如果我們不去看實現代碼,我們是不知道到底返回數組中哪個表示最小值,哪個表示最大值。因此我們可以創建一個類 Pair 類來容納最小值和最大值,而 Pair 名字比較大衆,其他程序員也可能使用這個名字,這樣就會造成混亂衝突。因此我們可以把它放到類 CalcMinMax 的內部作爲它的內部靜態類。這樣我們改造上述代碼如下:

public class CalcMinMax {
    public Pair calc(int... numbs) {
        int min = 0;
        int max = 0;

        for (int numb : numbs) {
            if (min > numb) {
                min = numb;
            }
            if (max < numb) {
                max = numb;
            }
        }

        return new Pair(min, max);
    }

    public static class Pair {
        private int min;
        private int max;

        public Pair(int min, int max) {
            this.min = min;
            this.max = max;
        }

        public int getMin() {
            return min;
        }

        public int getMax() {
            return max;
        }
    }
}

CalcMinMax.Pair pair = new CalcMinMax().calc(1, 4, 9, 3, 0);
System.out.println("最小值:" + pair.getMin());
System.out.println("最大值:" + pair.getMax());

如上所示,代碼的可讀性就更好,直接通過返回值對象的方法名就知道值的確切意思而不用再去翻看代碼。

對於靜態內部類,我們可以直接使用 OuterClass.StaticInnerClass 的方式來訪問。

內部類本質

內部類其實是一種編譯器現象,與虛擬機無關,編譯器將會把內部類編譯成文件名爲【外圍類名 + $ + 內部類名】形式的單獨的常規文件,而虛擬機則對此一無所知;而對於匿名內部類,編譯器會產生一個數字作爲其標識符,因此“內部類名”部分會是一個數字。

我們可以打開前邊示例生成的 class 文件目錄,看一下這些文件名。

閉包

閉包(closure)是一個可調用的對象,它記錄了一些信息,而這些信息來自於創建它的作用域。因此我們可以把內部類看作是面向對象的閉包。

應用練習

我們來看一個內部類應用示例:有一個固定大小的 Object 數組,以類的形式包裝起來類;通過方法 add 向序列末尾添加新的 Object。要獲取數組中的每一個元素對象,使用 Selector 接口。Selector 接口有如下3個方法:

  1. end() 檢查序列是否到末尾

  2. current() 返回當前對象

  3. next() 移動到序列的下一個對象

public interface Selector {
    boolean end();

    Object current();

    void next();
}
public class ArraySequence {
    private Object[] data;
    private int next = 0;

    public ArraySequence(int size) {
        data = new Object[size];
    }

    public void add(Object object) {
        if (next < data.length) {
            data[next++] = object;
        }
    }

    public SequenceSelector selector() {
        return new SequenceSelector();
    }

    public SequenceSelectorReverse selectorReverse() {
        return new SequenceSelectorReverse();
    }

    private class SequenceSelector implements Selector {
        private int index = 0;

        @Override
        public boolean end() {
            return index == data.length;
        }

        @Override
        public Object current() {
            if (index < data.length) {
                return data[index];
            }
            return null;
        }

        @Override
        public void next() {
            if (index < data.length) {
                index++;
            }
        }
    }

    private class SequenceSelectorReverse implements Selector {
        private int index;

        public SequenceSelectorReverse() {
            this.index = data.length - 1;
        }

        @Override
        public boolean end() {
            return index < 0;
        }

        @Override
        public Object current() {
            if (index > -1) {
                return data[index];
            }
            return null;
        }

        @Override
        public void next() {
            if (index > -1) {
                index--;
            }
        }
    }

    public static void main(String[] args) {
        ArraySequence as = new ArraySequence(10);
        for (int i = 0; i < 10; i++) {
            as.add("numb-" + (i + 1));
        }

        Selector selector = as.selector();
        while (!selector.end()) {
            System.out.println(selector.current());
            selector.next();
        }

        Selector selectorReverse = as.selectorReverse();
        while (!selectorReverse.end()) {
            System.out.println(selectorReverse.current());
            selectorReverse.next();
        }
    }
}

常見面試問題

內部類有哪些種類?

內部類和局部內部類的區別?

內部類的作用?

內部類可以被繼承嗎?

匿名內部類可以有靜態成員變量和靜態方法嗎?爲什麼?

下邊代碼執行結果

public class Outer {
    public int num = 1;

    class Inner {
        public int num = 2;

        public void show() {
            int num = 3;
            System.out.println(num);
            System.out.println(this.num);
            System.out.println(Outer.this.num);
        }
    }

    public static void main(String[] args) {
        Outer.Inner inner = new Outer().new Inner();
        inner.show();
    }
}

補全代碼,執行結果要求打印出字符串 helloworld

public class Outer {
    interface Inter {
        void show();
    }

    // TODO: 補全代碼

    public static void main(String[] args) {
        Outer.create().show();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章