1. 程式人生 > >小白學Java:內部類

小白學Java:內部類

目錄

  • 小白學Java:內部類
    • 內部類的分類
      • 成員內部類
      • 區域性內部類
      • 靜態內部類
      • 匿名內部類
    • 內部類的繼承
    • 內部類有啥用

小白學Java:內部類

內部類是封裝的一種形式,是定義在類或介面中的類。

內部類的分類

成員內部類

即定義的內部類作為外部類的一個普通成員(非static),就像下面這樣:

public class Outer {
    class Inner{
        private String id = "夏天";

        public String getId() {
            return id;
        }
    }

    public Inner returnInner(){
        return new Inner();
    }
    public void show(){
        Inner in = new Inner();
        System.out.println(in.id);
    }
}

我們通過以上一個簡單的示例,可以得出以下幾點:

  • Inner類就是內部類,它的定義在Outer類的內部。
  • Outer類中的returnInner方法返回一個Inner型別的物件。
  • Outer類中的show方法通過我們熟悉的方式建立了Inner示例並訪問了其私有屬性。

可以看到,我們像使用正常類一樣使用內部類,但實際上,內部類有許多奧妙,值得我們去學習。至於內部類的用處,我們暫且不談,先學習它的語法也不遲。我們在另外一個類中再試著建立一下這個Inner物件吧:

class OuterTest{
    public static void main(String[] args) {
        //!false:Inner in = new Inner();
        Outer o = new Outer();
        o.show();
        Outer.Inner in = o.returnInner();
        //!false: can't access --System.out.println(in.id);
        System.out.println(in.getId());
    }
}

哦呦,有意思了,我們在另一個類OuterTest中再次測試我們之前定義的內部類,結果出現了非常明顯的變化,我們陷入了沉思:

  • 我們不能夠像之前一樣,用Inner in = new Inner();建立內部類例項。
  • 沒關係,我們可以通過Outer物件的returnInner方法,來建立一個例項,成功!
  • 需要注意的是:我們如果需要一個內部類型別的變數指向這個例項,我們需要明確指明型別為:Outer.Inner,即外部類名.內部類名
  • 好啦,得到的內部類物件,我們試著直接去訪問它的私有屬性!失敗!
  • 那就老老實實地通過getId方法訪問吧,成功!

說到這,我們大概就能猜測到:內部類的存在可以很好地隱藏一部分具有聯絡程式碼,實現了那句話:我想讓你看到的東西你隨便看,不想讓你看的東西你想看,門都沒有。

連結到外部類

其實我們之前在分析ArrayList原始碼的時候,曾經接觸過內部類。我們在學習迭代器設計模式的時候,也曾領略過內部類帶了的奧妙之處。下面我通過《Java程式設計思想》上:通過一個內部類實現迭代器模式的簡單案例做相應的分析與學習:
首先呢,定義一個“選擇器”介面:

interface Selector {
    boolean end();//判斷是否到達終點
    void next();//移到下一個元素
    Object current();//訪問當前元素
}

然後,定義一個序列類Sequence:

public class Sequence {
    private Object[] items;
    private int next = 0;
    //構造器
    public Sequence(int size) {
        items = new Object[size];
    }
    public void add(Object x) {
        if (next < items.length) {
            items[next++] = x;
        }
    }
    //該內部類可以訪問外部類所有成員(包括私有成員)
    private class SequenceSelector implements Selector {
        private int i = 0;
        @Override
        public boolean end() {
            return i == items.length;
        }
        @Override
        public void next() {
            if (i < items.length) {
                i++;
            }
        }
        @Override
        public Object current() {
            return items[i];
        }
    }
    //向上轉型為介面,隱藏實現的細節
    public Selector selector() {
        return new SequenceSelector();
    }
}
  • 內部類SequenceSelector以private修飾,實現了Selector介面,提供了方法的具體實現。
  • 內部類訪問外部類的私有成員items,可以得出結論:內部類自動擁有對其外部類所有成員的訪問權。

當內部類是非static時,當外部類物件建立了一個內部類物件時,內部類物件會產生一個指向外部類的物件的引用,所以非static內部類可以看到外部類的一切。

  • 外部類Sequenceselector方法返回了一個內部類例項,意思就是用介面型別接收實現類的例項,實現向上轉型,既隱藏了實現細節,又利於擴充套件。

我們看一下具體的測試方法:

    public static void main(String[] args) {
        Sequence sq = new Sequence(10);
        for (int i = 0; i < 10; i++) {
            sq.add(Integer.toString(i));
        }
        //產生我們設計的選擇器
        Selector sl = sq.selector();

        while (!sl.end()) {
            System.out.print(sl.current() + " ");
            sl.next();
        }
    }
  • 隱藏實現細節:使用Sequence序列儲存物件時,不需要關心內部迭代的具體實現,用就完事了,這正是內部類配合迭代器設計模式體現的高度隱藏。
  • 利於擴充套件:我們如果要設計一個反向迭代,可以在Sequence內部再定義一個內部類,並提供Selector介面的實現細節,及其利於擴充套件,妙啊。

.new和.this

我們稍微修改一下最初的Outer:

public class Outer {
    String id = "喬巴";
    class Inner{
        private String id = "夏天";

        public String getId() {
            return id;
        }
        public String getOuterId(){
            return Outer.this.id;
        }
        public Outer returnOuter(){
            return Outer.this;
        }
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        System.out.println(o.new Inner().getId());//夏天
        System.out.println(o.new Inner().getOuterId());//喬巴
    }
}
  • 在內部類Inner體內添加了returnOuter的引用,return Outer.this;,即外部類名.this
  • 我們可以發現,內部類內外具有同名的屬性,我們在內部類中,不加任何修飾的情況下預設呼叫內部類裡的屬性,我們可以通過引用的形式訪問外部類的id屬性,即Outer.this.id

我們來測試一波:

    public static void main(String[] args) {
        Outer.Inner oi = new Outer().new Inner();
        System.out.println(oi.getId());//夏天
        Outer o = oi.returnOuter();
        System.out.println(o.id);//喬巴
    }
  • 外部類產生內部類物件的方法已經被我們刪除了,這時我們如果想要通過外部類物件建立一個內部類物件:Outer.Inner oi = new Outer().new Inner();,即在外部類物件後面用.new 內部類構造器

我們對內部類指向外部類物件的引用進行更加深入的理解與體會,我們會發現,上面的程式碼在編譯之後,會產生兩個位元組碼檔案:Outer$Inner.classOuter.class。我們對Outer$Inner.class進行反編譯:

確實,內部類在建立的過程中,依靠外部類物件,而且會產生一個指向外部類物件的引用。

區域性內部類

方法作用域內部類

即在方法作用域內建立一個完整的類。

public class Outer {
    public TestOuter test(final String s){
        class Inner implements TestOuter{
            @Override
            public void testM() {
                //!false: s+="g";
                System.out.println(s);
            }
        }
        return new Inner();
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        o.test("天喬巴夏").testM();//天喬巴夏
    }
}
interface TestOuter{
    void testM();
}

需要注意兩點:

  • 此時Inner類是test方法的一部分,Outer不能在該方法之外訪問Inner。
  • 方法傳入的引數s和方法內本身的區域性變數都需要以final修飾,不能被改變!!!

JDK1.8之後可以不用final顯式修飾傳入引數和區域性變數,但其本身還是相當於final修飾的,不可改變。我們去掉final,進行反編譯:

任意作用域內的內部類

可以將內部類定義在任意的作用域內:

public class Outer {
    public void test(final String s,final int value){
        final int a = value;
        if(value>2){
            class Inner{
                public void testM() {
                    //!false: s+="g";
                    //!false: a+=1;
                    System.out.println(s+", "+a);
                }
            }
            Inner in = new Inner();
            in.testM();
        }
        //!false:Inner i = new Inner();
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        o.test("天喬巴夏",3);
    }
}

同樣需要注意的是:

  • 內部類定義在if條件程式碼塊中,並不意味著建立該內部類有相應的條件。內部類一開始就會被建立,if條件只決定能不能用裡頭的東西。
  • 如上所示,if作用域之外,編譯器就不認識內部類了,因為它藏起來了。

靜態內部類

即用static修飾的成員內部類,歸屬於類,即它不存在指向外部類的引用。

public class Outer {
    static int a = 5;
    int b = 6;
    static class Inner{
        static int value;
        public void show(){
            //!false System.out.println(b);
            System.out.println(a);
        }
    }
}
class OuterTest {
    public static void main(String[] args) {
        Outer.Inner oi = new Outer.Inner();
        oi.show();
    }
}

需要注意的是:

  • 靜態內部類也可以定義非靜態的成員屬性和方法。
  • 靜態內部類物件的建立不依靠外部類的物件,可以直接通過:new Outer.Inner()建立內部類物件。
  • 靜態內部類中可以包含靜態屬性和方法,而除了靜態內部類之外,即我們上面所說的所有的內部類內部都不能有(但是可以有靜態常量static final修飾)。
  • 靜態內部類不能訪問非靜態的外部類成員。
  • 最後,我們反編譯驗證一下:

匿名內部類

這個型別的內部類,看著名字就怪怪的,我們先看看一段違反我們認知的程式碼:

public class Outer {
    public InterfaceInner inner(){
    //建立一個實現InterfaceInner介面的是實現類物件
        return new InterfaceInner() {
            @Override
            public void show() {
                System.out.println("Outer.show");
            }
        };
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        o.inner().show();
    }
}
interface InterfaceInner{
    void show();
}

真的非常奇怪,乍一看,InterfaceInner是個介面,而Outer類的inner方法怎麼出現了new InterfaceInner()的字眼呢?介面不是不能建立例項物件的麼?

確實,這就是匿名內部類的一個使用,其實inner方法返回的是實現了介面方法的實現類物件,我們可以看到分號結尾,代表一個完整的表示式,只不過表示式包含著介面實現,有點長罷了。所以上面匿名內部類的語法其實就是下面這種形式的簡化形式:

public class Outer {   
    class Inner implements InterfaceInner{
        @Override
        public void show(){
            System.out.println("Outer.show");
        }
    }
    public InterfaceInner inner(){ 
        return new Inner();  
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        o.inner().show();
    }
}
interface InterfaceInner{
    void show();
}

不僅僅是介面,普通的類也可以被當作“介面”來使用:

public class Outer {
    public OuterTest outerTest(int value) {
        //引數傳給匿名類的基類構造器
        return new OuterTest(value) {
            
            @Override
            public int getValue() {
                return super.getValue() * 10;
            }
        };
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        System.out.println(o.outerTest(10).getValue());//100
    }
}
class OuterTest {
    public int value;
    OuterTest(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}

需要注意的是:

  • 匿名類既可以擴充套件類,也可以實現介面,當然抽象類就不再贅述了,普通類都可以,抽象類就更可以了。但不能同時做這兩件事,且每次最多實現一個介面。
  • 匿名內部類沒有名字,所以自身沒有構造器。
  • 針對類而言,上述匿名內部類的語法就表明:建立一個繼承OuterTest類的子類例項。所以可以在匿名內部類定義中呼叫父類方法與父類構造器。
  • 傳入的引數傳遞給構造器,沒有在類中直接使用,可以不用在引數前加final。

內部類的繼承

內部類可以被繼承,但是和我們普通的類繼承有些出處。具體來看一下:

public class Outer {
    class Inner{
        private int value = 100;
        Inner(){
        }
        Inner(int value){
            this.value = value;
        }
        public void f(){
            System.out.println("Inner.f "+value);
        }
    }
}
class TestOuter extends Outer.Inner{
    TestOuter(Outer o){
        o.super();
    }
    TestOuter(Outer o,int value){
        o.super(value);
    }

    public static void main(String[] args) {
        Outer o = new Outer();
        TestOuter tt = new TestOuter(o);
        TestOuter t = new TestOuter(o,10);
        tt.f();
        t.f();
    }
}

我們可以發現的是:

  • 一個類繼承內部類的形式:class A extends Outer.Inner{}
  • 內部類的構造器必須連結到指向外部類物件的引用上,o.super();,即都需要傳入外部類物件作為引數。

內部類有啥用

可以看到的一點就是,內部類內部的實現細節可以被很好地進行封裝。而且Java中存在介面的多實現,雖然一定程度上彌補了Java“不支援多繼承”的特點,但內部類的存在使其更加優秀,可以看看下面這個例子:

//假設A、B是兩個介面
class First implements A{
    B makeB(){
        return new B() {
        };
    }
}

這是一個通過匿名內部類實現介面功能的簡單的例子。對於介面而言,我們完全可以通過下面這樣進行,因為Java中一個類可以實現多個介面:

class First implements A,B{
}

但是除了介面之外,像普通的類,像抽象類,都可以定義獨立的內部類去單獨繼承並實現,使用內部類使“多重繼承”更加完善。


由於後面的許多內容還沒有涉及到,學習到,所以總結的比較淺顯,並沒有做特別深入,特別真實的場景模擬,之後有時間會再做系統性的總結。如果有敘述錯誤的地方,還望評論區批評指標,共同進步。
參考:
《Java 程式設計思想》
https://stackoverflow.com/questions/70324/java-inner-class-and-static-nested-class?r=SearchRes