Java中的內部類

2020-08-10 00:25:09

將一個類的定義放在另一個類的定義內部,這就是內部類;內部類允許你把一些邏輯相關的類組織在一起,並控制位於內部的類的可視性;

內部類的使用

1.1 建立內部類

把類的定義置於外圍類的裏面:

public class Parcel{
    class Contents{
        private int i = 11;
        public int value(){ return i;}
    }
    public Contents contents(){
        return new Contents();
    }
    public static void main(String[] args){
        Parcel p = new Parcel();
        Parcel.Contents c = p.contents();
    }

}

其實上使用內部類與使用普通類相比較沒有什麼不同,但是因爲內部類是巢狀在類中的,我們可以在外部類中定義一個方法用來返回內部類中的參照。如果想從外部類的非靜態方法之外的任意位置建立某個內部類的物件,那麼必須像在main()方法中那樣,具體地指明這個物件的型別:OuterClassName.InnerClassName

1.2 鏈接到外部類

內部類似乎只是一種名字隱藏和組織程式碼的模式;當生成一個內部類的物件時,此物件與製造它的外圍物件之間就有了一種聯繫,所以它能存取其外圍物件的所有成員,而不需要任何特殊條件。

內部類還擁有其外圍類的所有成員的存取權,內部類的物件只能在與其外圍類的物件相關聯的情況下才能 纔能被建立。構建內部類物件時,需要一個指向其外圍類物件的參照,如果編譯器存取不到這個參照就會報錯。

1.3 生成對外部類物件的參照

如果你需要生成對外部類物件的參照,可以使用外部類的名字後面緊跟 .this。這樣產生的參照自動具有正確的型別,這一點在編譯期就被知曉並受到檢查,因此沒有任何執行時開銷。

在外部使用內部類的兩種方式:
1.3.1 在外圍類中定義一個方法,該方法返回內部類的一個物件,方法是public的

public class DotThis{
    void f(){
        System.out.println("DotThis.f()");
    }
    public class Inner{
        public DotThis outer(){
            return DotThis.this;
        }
    }
    public Inner inner(){
        return new Inner();
    }
    public static void main(String[] args){
        DotThis dt = new DotThis();
        DotThis.Inner dti = dt.inner();
        dti.outer().f();
    }
}       

1.3.2 用外圍類的物件得到內部類物件

有時候你可能想要告知某些其他物件,去建立其某個內部類的物件。要實現此目的,你必須在new表達式中提供對其他外部類物件的參照,這時需要使用 .new語法:

public class DotNew{
    public class Inner{}
    public static void main(String[] args){
        DotNew dn = new DotNew();
        DotNew.Inner dni = dn.new Inner();
    }
}

要想直接建立內部類的物件,不能去參照外部類的名字DotNew,而是必須使用外部類的物件來建立該內部類物件。這也解決了內部類名字作用域的問題,因此你不必宣告,dn.new DotNew.Inner()

在擁有外部類物件之前是不可能建立內部類物件的。這是因爲內部類物件會暗暗地連線到建立它的外部類物件上。但是,如果你建立的是巢狀類(靜態內部類),那麼它就不需要對外部類物件的參照。

1.4 內部類與向上轉型

當將內部類向上轉型爲其基礎類別,尤其是轉型爲一個介面的時候,內部類就有了用武之地。這是因爲此內部類–某個介面的實現–能夠完全不可見,並且不可用。所得到的只是指向基礎類別或介面的參照,所以能夠很方便地隱蔽實現細節。

public interface Destination{
     String readLabel();  
}
public interface Contents{
     int value();
}

現在ContentsDestination表示用戶端程式設計師可用的介面。當取的得了一個指向基礎類別或介面的參照時,甚至可能無法找出它確切的型別:

class Parcel{
    private class PContents implements Contents{
        private int i =11;
        public int value(){return i;}
    }
    protected class PDestination implements Destination{
        private String label;
        private PDestination(String whereTo){
            label  = whereTo;
        }
     public String readLabel(){ return label;}
    }
   
          
    public Destination destination(String s){
        return new PDestination(s); 
    }
    public Contents  contents(){
      return new PContents(); 
  }

}
public class TestParcel{
  public static void main(String[] args){
    Parcel p = new Parcel();
    Contents c = p.contents();
    Destination d = p.destination("Tasmania");
  }
}     

Parcel中增加了一些新東西:內部類PContentsprivate,所以除了Parcel,沒人能存取它。PDestinationprotected,所以只有Parcel及其子類、還有與Parcel同一個包中的類能存取PDestination,其他類都不能存取PDestination。這意味着,如果用戶端程式設計師想瞭解或存取這些成員,那是受限制的。

private內部類給類的設計者提供了一種途徑,通過這種方式可以完全組織任何依賴於型別的編碼,並且完全隱藏了實現的細節。此外,從用戶端程式設計師的角度來看,由於不能存取任何新增加的、原本不屬於公共介面的方法,所以擴充套件介面是沒有價值的。這也給Java編譯器提供了生成更高效程式碼的機會。

內部類分類

上邊已經簡單介紹了內部類的使用,除了作爲外部類的一個成員存在的內部類(成員內部類)以外還有其他形式的內部類:區域性內部類、匿名內部類

2.1 區域性內部類

在Java中,可以在一個方法裏面或者在任意的作用域內定義內部類。這麼做的兩個理由:

  • 1.實現了某型別的介面,於是可以建立並返回對其的參照。
  • 2.需要解決一個複雜的問題,想建立一個類來輔助解決方案,但是又不希望這個類是公共可用的。

在方法的作用域內建立一個完整的類,這稱作區域性內部類

public class Parcel5{

    interface Destination{}

    public Destination destination(String s){
        class PDestination implements Destination {
            private String label;
            private PDestination(String whereTo) {
                label = whereTo;
            }
            public String readLabel(){return label;}
        }
        return new PDestination(s);
    }

    public static void main(String[] args) {
        Parcel5 p = new Parcel5();
        Destination d = p.destination("Tasmania");
    }

}

在任意的作用域內嵌入一個內部類:

public class Parcel6 {
    private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s){
                    id=s;
                }
                String getSlip(){return id;}
            }
            TrackingSlip ts=new TrackingSlip("slip");
            String s=ts.getSlip();
        }
    }
    public void track(){
        internalTracking(true);
    }
    public static void main(String[] args) {
        Parcel6 p=new Parcel6();
        p.track();
    }
}

當我們需要一個已命名的構造器,或者需要過載構造器時我們會用到區域性內部類,而不能使用匿名內部類,因爲匿名內部類只能用於範例初始化。當我們需要不止一個該內部類的物件,也要採用區域性內部類。

區域性內部類不能有存取說明符,因爲它不是外圍類的一部分;但是它可以訪問當前程式碼塊內的常數,以及此外圍類的所有成員。區域性內部類的名字在方法外是不可見的。

2.2 匿名內部類

public class Parcel {

    interface Contents {}

//    class MyContents implements Contents {
//        private int i=11;
//        public int value(){return i;} 
//    }

    public Contents contents() {
        return new Contents() {
            private int i = 11;
            public int value() {return i;}
        };
        
//        return new MyContents();
    }
    public static void main (String[]args){
            Parcel p = new Parcel();
            Contents c = p.contents();

    }

}

建立一個繼承自Contents的匿名類的物件,在這個匿名內部類中,使用了預設的構造器來生成Contents。如果定義一個匿名內部類,並且希望它使用一個在其外部定義的物件,那麼編譯器會要求其參數是final的。否則,編譯器將會報錯。如果只是傳遞給匿名類的基礎類別的構造器,那麼不需要將傳入的形參定爲final(匿名內部類沒有構造器,只能用範例化代替。)

2.3 巢狀類

如果不需要內部類物件與其外圍類物件之間有聯繫,那麼可以將內部類宣告爲static。這通常稱爲巢狀類。普通的內部類物件隱式地儲存了一個參照,指向建立它的外圍類物件。然而,當內部類是static的時,就會不一樣了。巢狀類意味着:

  • 1.要建立巢狀類的物件,並不需要其外圍類的物件
  • 2.不能從巢狀類的物件中存取非靜態的外圍類物件

巢狀類與普通的內部類還有一個區別,普通內部類的欄位與方法,只能放在類的外部層次上,所以普通的內部類不能有static數據和static欄位,也不能包含巢狀類。

介面內部的類:

正常情況下,不能在介面內部放置任何程式碼,但巢狀類可以作爲介面的一部分。你放到介面中的任何類都自動地是public和static的。因爲類是static的,只是將巢狀類置於介面的名稱空間內,這並不違反介面的規則。你甚至可以在內部類中實現其外圍介面:

public interface ClassInInterface{
       void howdy();
       class Test implements ClassInInterface{
        public void howdy(){
     System.out.print("Howdy!");
       }
        public static void main(String[] args){
      new Test().howdy();
        }
   }
}

如果你想要建立某些公共程式碼,使得它們可以被某個介面的所有不同實現所共用,那麼使用介面內部的巢狀類會顯得很方便。

巢狀類的兩個有效用途:

  • 1.實現每個類實現介面都需要一部分公共程式碼
  • 2.代替main方法對每個類進行測試

從多層巢狀類中存取外部類的成員:
一個內部類被巢狀多少層並不重要—它能透明地存取所有它所嵌入的外圍類的所有成員,如下所示:

class MNA{
     private void f(){}
     class A{
        private void g() {}
        public  class B{
            void h(){
                 g();
                 f();
            }
        }
     }    
}

public class MultiNestingAccess{
      public static void main(String[] args){
             MNA mna = new MNA();
             MNA.A mnaa = mna.new A();
             MNA.A.B mnaab = mnaa.new B();
             mnaab.h();
      }
}    

可以看到,在MNA.A.B中,呼叫方法g()和f()不需要任何條件(即使它們被定義爲private)。

其他

3.1 爲什麼需要內部類

一般來說,內部類繼承自某個類或實現某個介面,內部類的程式碼操作建立它的外圍類的物件。所以可以認爲內部類提供了某種進入其外圍類的視窗

在外圍類中不是總能享用到介面帶來的方便,有時需要我們去實現介面。而使用內部類時,每個內部類都能獨立地繼承自一個介面的實現,所以無論外圍類是否已經繼承了某個介面的實現,對於內部類都沒有影響。如果沒有內部類提供的、可以繼承多個具體的或抽象的類的能力,一些設計與程式設計問題就很難解決。從這個角度看,內部類使得多重繼承的解決方案變得完整。

介面解決了部分問題,而內部類有效地實現了 「多重繼承」。也就是說,內部類允許繼承多個非介面型別(類或抽象類)。

使用內部類可以獲得的一些特性:

  • 1.內部類可以有多個範例,每個範例都有自己的狀態資訊,並且與其外圍類物件的資訊相互獨立。
  • 2.在單個外圍類中,可以讓多個內部類以不同的方式實現同一個介面,或繼承同一個類。
  • 3.建立內部類物件的時刻並不依賴於外圍類物件的建立。
  • 4.內部類並沒有令人迷惑的「is-a」關係;它就是一個獨立的實體。

3.2 閉包與回撥

閉包是一個可呼叫的物件,它記錄了一些資訊,這些資訊來自於建立它的作用域。通過這個定義,可以看出內部類是物件導向的閉包,因爲它不僅包含外圍類(建立內部類的作用域)的資訊,還自動擁有一個指向此外圍類物件的參照,在此作用域內,內部類有權操作所有的成員,包括private成員。通過內部類提供閉包的功能是優良的解決方案,它比指針更靈活、更安全。

3.3 內部類的繼承

因爲內部類的構造器必須連線到指向其外圍類物件的參照,所以在繼承內部類的時候,事情就變得有點複雜。問題在於,那個指向外圍類物件的「祕密的」參照必須被初始化,而在導出類中不再存在可連續的預設物件。要解決這個問題,必須使用特殊的語法來明確說清它們之間的關聯:

class WithInner{
     class Inner {}
}

public classs InheritInner extends WithInner.Inner{
     InheritInner(WithInner wi){
           wi.super();
     }
     public static void main(String[] args){
          WithInner wi = new WithInner();
          InheritInner ii = new InheritInner(wi);
     }  
}

可以看到,InheritInner只繼承自內部類,而不是外圍類。但是當要生成一個構造器時,預設的構造器並不算好,而且不能只是傳遞一個指向外圍類物件的參照。此外,必須在構造器內使用如下語法:
enclosingClassReference.super(); 這樣才能 纔能提供了必要的參照,然後程式才能 纔能編譯通過。

3.4 內部類的覆蓋問題

內部類不可以像普通方法覆蓋那樣進行覆蓋
覆蓋內部類的方法:首先外圍類繼承外圍類,然後內部類繼承外圍類(外圍類.內部類方式)

3.5 內部類識別符號

由於每個類都會產生一個 .class檔案,其中包含瞭如何建立該型別的物件的全部資訊,內部類也必須生成一個 .class檔案以包含它們的Class物件資訊。
這些類檔案的命名有嚴格的規則:外圍類的名字,加上 「$」,再加上內部類的名字。例如:LocalInnerClass.java生成的.class檔案包括:
Counter.class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class

如果內部類是匿名的,編譯器會簡單地產生一個數字作爲其識別符號。如果內部類是巢狀在別的內部類之中,只需要直接將它們的名字加在其外圍類識別符號與"$"的後面。