1. 程式人生 > >Java內部類整理筆記——一篇讀懂內部類

Java內部類整理筆記——一篇讀懂內部類

Java內部類整理筆記

一 . 內部類的總結

      一般來說,內部類繼承自某個類或實現某個介面,內部類的程式碼操作建立它的外圍類的物件。所以,可以認為內部類提供了某種進入其外圍類的視窗。每個內部類都能獨立地繼承自一個介面的實現,所以無論外圍類是否已經繼承了某個介面的實現,對於內部類都沒有影響。因為內部類提供了可以繼承多個具體的或抽象的類的能力,使得設計與程式設計問題得以很好地解決。從這個角度看,內部類使得多重繼承的解決方案變得完整。介面解決了部分問題,而內部類有效地實現了“多重繼承”。也就是說內部類允許繼承多個類或抽象類。

二 . 內部類的定義

(1)內部類就是將一個類的定義放在另一個類的定義內部。其中,包含內部類的類被稱為外部類(或者叫宿主類)。

(2)內部類定義語法格式如下:

public class OuterClass 
{
   
    //此處可以定義內部類
}

三 . 內部類的結構圖

內部類使用說明書:

  • 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中的其他類訪問該類。
  • 內部類成員可以直接訪問外部類的私有資料,因為內部類被當成其外部類成員,同一個類的成員之間可以互相訪問。但外部類不能直接訪問內部類的實現細節,例如內部類的成員變數。
  • 匿名內部類適合用於建立那些僅需要使用一次的類。
  • 內部類比外部類可以多使用三個修飾符:private 、protected 、static ——外部類不能使用這三個修飾符。
  • 非靜態內部類不能擁有靜態成員。

下面按照這個結構圖,一一詳細介紹內部類的基礎知識。

四 . 非靜態內部類

      成員內部類是一種與成員變數、方法、構造器和初始化塊相似的類成員,分為兩種:非靜態內部類和靜態內部類,沒有使用 static 修飾的成員內部類是非靜態內部類,有 static 修飾的成員內部類是靜態內部類。因為內部類作為其外部類的成員,所以可以使用任意訪問控制符如:private 、 protected 和public 等修飾。下面使用一個例子來分析非靜態內部類的特點:

public class Person {

    private int age;
//外部類的兩個過載的構造器
public Person(){}
    public 
Person(int age) { this.age=age; } //--------------------------------------------------------------------- //定義一個非靜態內部類 private class Student { //非靜態內部類的兩個例項變數 private String name; private String sex; //非靜態內部類的兩個過載的構造器 public Student(){} public Student(String name,String sex) { this.name=name; this.sex=sex; } //下面省略 name 、sex 的 setter 和getter 方法 //…… //非靜態內部類的例項方法 public void info() { System.out.println("當前學生的名字是:" + name + ",性別:" + sex); //直接訪問外部類的 private 修飾的成員變數 System.out.println("當前學生的年齡是:" + age); } } //--------------------------------------------------------------------- public void test() { Student student=new Student("張三","男"); student.info(); } public static void main(String [] args) { Person person=new Person(20); person.test(); } }
      上面程式中虛線中的部分就是一個使用 private 修飾的內部類,在外部類裡使用非靜態內部類時,與平時使用普通類差不多,首先例項化物件,再呼叫物件裡的方法,如:Student student=new Student ("張三","男") ; student . info() ; 所示。
      編譯上面程式,看到在檔案所在路徑生成了兩個 class 檔案,一個是 Person . class ,另一個是 Person$Student . class ,前者是外部類 Person 的 class 檔案,後者是內部類 Student 的 class 檔案,即成員內部類(包括靜態內部類、非靜態內部類)的 class 檔案總是這種形式:OuterClass$InnerClass . class 。

      在非靜態內部類裡可以直接訪問外部類的 private 成員,如上面程式中在 Student 類的方法內直接訪問“當前學生的年齡”,即就是直接訪問 Person 中的 age 變數。這是因為在非靜態內部類物件裡,儲存了一個它所寄生的外部類的引用(當呼叫非靜態內部類的例項方法時,必須有一個非靜態內部類例項,非靜態內部類例項必須寄生在外部類例項裡)。上面程式執行時的記憶體示意圖如下:


非靜態內部類物件中保留外部類物件的引用記憶體示意圖

五 . 使用 .this  

      當在非靜態內部類的方法內訪問某個變數時,系統優先在該方法內查詢是否存在該名字的區域性變數,如果存在久使用該變數;如果不存在,則到該方法所在的內部類中查詢是否存在該名字的成員變數,如果存在則使用該成員變數;如果不存在,則到該內部類所在的外部類中查詢是否存在該名字的成員變數,如果存在則使用該成員變數;如果不存在,系統將出現編譯錯誤:提示找不到該變數。因此,如果外部類成員變數、內部類成員變數與內部類裡方法的區域性變數同名,則可通過使用 this 、外部類類名 . this 作為限定來區分。下面通過程式碼來演示:

public class OuterClass {
    private String name="外部類的例項變數";
    private class InnerClass
    {
        private String name="內部類的例項變數";
        public void info()
        {
            String name="內部類裡區域性變數";
//通過外部類類名.this.VarName 訪問外部類例項變數
System.out.println("外部類的例項變數:"+OuterClass.this.name);
//通過 this.VarName 訪問內部類例項的變數
System.out.println("內部類的例項變數:"+this.name);
//直接訪問區域性變數
System.out.println("內部類裡區域性變數:"+name);
}
    }
    public void test()
    {
        InnerClass innerClass=new InnerClass();
innerClass.info();
}
    public static void main(String [] args)
    {
        new OuterClass().test();
}
}

      上面程式中通過 OutterClass.this.propName 的形式訪問外部類的例項變數,通過 this.propName 的形式訪問非靜態內部類的例項變數。

六 . 使用 .New

      非靜態內部類的成員可以訪問外部類的 private 成員,但反過來就不成立。非靜態內部類的成員只在非靜態內部類範圍內是可知的,並不能被外部類直接使用。可以認為外部類物件訪問非靜態內部類成員時,可能非靜態普通內部類物件根本不存在。因此如果外部類需要訪問非靜態內部類的成員,則必須顯式建立非靜態內部類物件來呼叫訪問其例項成員。下面通過程式碼來分析:

public class OuterClass {
    private int outer=3;
//------------------------------------------------------
private class InnerClass
    {
        private int inner=9;
        public void info()
        {
            //非靜態內部類可以直接訪問外部類的private成員變數
System.out.println("外部類的例項變數:"+outer);
}
    }
    //-----------------------------------------------------------
public void test()
    {
        //外部類不能直接訪問非靜態內部類的例項變數,必須顯示建立內部類物件
System.out.println("內部類的inner值"+new InnerClass().inner);  
}
    public static void main(String [] args)
    {
        new OuterClass().test();
}
}

      Java規定,靜態成員不能訪問非靜態成員的規則,因此,外部類的靜態方法、靜態程式碼塊不能訪問非靜態內部類,包括不能使用非靜態內部類定義變數、建立例項等。總之,不允許在外部類的靜態成員中使用非靜態內部類。同時,Java不允許在非靜態內部類裡定義靜態成員,包括不能有靜態方法、靜態成員變數、靜態初始化塊。

七 . 靜態內部類

      使用 static 修飾的內部類被稱為類內部類,或是靜態內部類。靜態內部類屬於外部類本身,而不屬於尾部類的某個物件。靜態內部類可以包含靜態成員,也可以包含非靜態成員。根據靜態成員不能訪問非靜態成員的規則,靜態內部類不能訪問外部類的例項成員,只能訪問外部類的類成員。

     靜態內部類是外部類的一個靜態成員,因此外部類的所有方法、所有初始化塊中可以使用靜態內部類來定義變數、建立物件等。但外部類依然不能直接訪問靜態內部類的成員,但可以使用靜態內部類的類名作為呼叫者來訪問靜態內部類的類成員,也可以使用靜態內部類物件作為呼叫者來訪問靜態內部類的例項成員。下面用程式來說明:

public class OuterClass {
    static class InnerClass
    {
        private static int inner1=9;
        private  int inner2=10;
        public void info()
        {
           //通過類名訪問靜態內部類的類成員
System.out.println(InnerClass.inner1);
//通過例項訪問靜態內部類的例項成員
System.out.println(new InnerClass().inner2);
}
    }
}

     Java還允許在接口裡定義內部類,接口裡定義的內部類預設使用 public static 修飾,也就是說,介面內部類只能是靜態內部類。如果為介面內部類指定訪問修飾符,則只能指定 public 訪問修飾符;如果定義介面內部類時省略訪問控制符,則該內部類預設是 public 訪問控制權限。

八 . 使用內部類

      定義類的主要作用就是定義變數、建立例項和作為父類被繼承,定義內部類的主要作用也是如此。下面分三種情況討論內部類的用法。

1 . 在外部類內部使用內部類                                                                                                                  

      在外部類內部類使用內部類時,與平常使用普通類沒太大的區別。一樣可以直接通過內部類類名來定義變數,通過 new 呼叫內部類構造器來建立例項。   

2 . 在外部類以外使用非靜態內部類

      在外部類以外的地方訪問內部類(包括靜態和非靜態兩種),則內部類不能使用 private 訪問控制權限,private 修飾的內部類只能在外部類內使用,對於使用其它訪問控制修飾符的內部類,則能在訪問控制符對應的訪問許可權內使用,

  • 省略訪問控制符的內部類,只能被與外部類處於同一個包中的其他類所訪問。
  • 使用 protected 修飾的內部類,可被與外部類處於同一個包中的其他類和外部類的子類所訪問。
  • 使用 public 修飾的內部類,可以在任何地方被訪問。

在外部類以外的地方定義內部類(包括靜態和非靜態兩種)變數的語法格式如下:

OuterClass . InnerClass . varName

     (如果外部類有包名,則還應該增加包名字首。)

      由於非靜態內部類的物件必須寄生在外部類的物件裡,因此建立非靜態內部類物件之前,必須先建立其外部類物件。在外部類以外的地方建立非靜態內部類例項的語法如下:

OuterInstance . new InnerConstructor ( )

下面看具體的程式:

class OutClass
{
    //定義一個內部類,不使用訪問修飾符
    //即只有同一個包中的其他類可以訪問該內部類
class InClass
    {
        public InClass(String str)
        {
            System.out.println(str);
}
    }
}
public class InnerDemo {
    public static void main(String [] args)
    {
        OutClass.InClass in=new OutClass().new InClass("內部類測試資料"); 
/*
        上面程式碼可改為如下三行程式碼
        1.使用 OutterClass . InnerClass 的形式定義內部類
        OutClass.InClass in;
        2.建立外部類例項,非靜態內部類例項寄生在該例項中
        OutClass out=new OutClass();
        3.通過外部類例項和 new 來呼叫內部類構造器建立非靜態內部類例項
        in =out.new InClass("內部類測試資料");
         */
}
   
}

      上面程式說明了,如果需要在外部類以外的地方建立非靜態內部類的子類,則非靜態內部類的構造器必須通過其外部類物件來呼叫。

      當建立一個子類時,子類構造器總會呼叫父類的構造器,因此在建立非靜態內部類的子類時,必須保證讓子類構造器可以呼叫非靜態內部類的構造器,呼叫非靜態內部類的構造器時,必須存在一個外部類物件。看下面的程式:

class SecClass extends OutClass.InClass
{
    //顯示定義 SecClass 的構造器
public SecClass(OutClass outClass) {
        //通過傳入的 OutClass 物件顯示呼叫 InClass 的構造器
outClass.super("內部類子類測試資料");
}
}

      從上面的程式碼可以看出,非靜態內部類 InClass 類的構造器必須使用外部類物件來呼叫,程式碼中 super 代表呼叫 InClass 類的構造器,而 OutClass 則代表外部類物件。如果建立 SecClass 物件時,必須先建立一個 OutClass 物件。因為 SecClass 是非靜態內部類 InClass 類的子類,非靜態內部類 InClass 物件裡必須有一個對 OutClass 物件的引用,其子類 SecClass 物件裡也應該持有對 OutClass 物件的引用。當建立 SecClass 物件時傳給構造器的 OutClass 物件,就是 SecClass 物件裡 OutClass 物件引用所指向的物件。既然非靜態內部類 InClass 物件和 SecClass 物件都必須持有指向 OutClass 物件的引用。那這兩者之間的區別又是什麼呢?區別是建立這兩種物件時傳入 OutClass 物件的方式不同:當建立非靜態內部類 InClass 類的物件時,必須通過OutClass 物件來呼叫 new 關鍵字;當建立 SecClass 類的物件時,必須使用 OutClass 物件作為呼叫者來呼叫 InClass 類的構造器。

3 . 在外部類以外使用靜態內部類

      因為靜態內部類時外部類類相關的,因此建立靜態內部類物件時無須建立外部類物件。在外部類以外的地方建立靜態內部類例項的語法如下:

new OuterClass . InnerConstructor ( )

     下面程式示範瞭如何在外部類以外的地方建立靜態內部類的例項。

class StaticOutClass
{
    //定義一個靜態內部類,不使用訪問修飾符
    //即只有同一個包中的其他類可以訪問該內部類
static class StaticInClass
    {
        public StaticInClass()
        {
            System.out.println("靜態內部類的構造器");
}
    }
}

public class InnerDemo {
    public static void main(String [] args)
    {
        StaticOutClass.StaticInClass in =new StaticOutClass.StaticInClass();
/*
        上面程式碼可改為如下兩行
        1.使用 StaticOutClass.StaticInClass 的形式定義內部類變數
        StaticOutClass.StaticInClass in ;
        2.通過 new 來呼叫內部類構造器建立靜態內部類例項
        new StaticOutClass.StaticInClass();
        * */
}
}

      從上面程式碼中可以看出來,不管是靜態內部類還是非靜態內部類,它們宣告變數的語法完全一樣。區別只是在建立內部類物件時,靜態內部類只需使用外部類即可呼叫構造器,而非靜態內部類必須使用外部類物件來呼叫構造器。

     因為呼叫靜態內部類的構造器時無須使用外部類物件,所以建立靜態內部類的子類也比較簡單,下面的程式碼就為靜態內部類StaticInClass 類定義一個空的子類。

class StaticSecClass extends StaticOutClass.StaticInClass
{
    
}

      從上面程式碼中可以看出來,當定義一個靜態內部類時,其外部類非常像一個包空間。

      相比之下,使用靜態內部類比使用非靜態內部類要簡單得多,只要把外部類當成靜態內部類的包空間即可。因此當程式需要使用內部類時,應該優先考慮使用靜態內部類。

九 . 區域性內部類

      如果把一個內部類放在方法裡定義,則這個內部類就是一個區域性內部類,區域性內部類僅在該方法裡有效。由於區域性內部類不能在外部類的方法以外的地方使用,因為區域性內部類也不能使用訪問控制符和 static 修飾符修飾。

      如果需要用區域性內部類定義變數、建立例項或派生子類,那麼都只能在區域性內部類所在的方法內進行。下面程式建立了局部內部類如下:

public class InnerDemo {
    public static void main(String [] args)
    {
        //定義區域性內部類
class InnerClass
        {
            int a;
}
        //定義區域性內部類的子類
class OneClass extends InnerClass
        {
            int b;
}
        //建立區域性內部類的物件
OneClass oneClass=new OneClass();
oneClass.a=5;
oneClass.b=8;
System.out.println("OneClass物件的a和b例項變數是: "+oneClass.a+oneClass.b);
}
}

  編譯上面程式,看到生成了三個 class 檔案:InnerDemo.class 、 InnerDemo$1InnerClass.class 和  InnerDemo$OneClass.class ,這表明區域性內部類的 class 檔案總是遵循如下的命名格式:OuterClass$NInnerClass.class 。注意到區域性內部類的 class 檔案的檔名比成員內部類的 class 檔案的檔名多了一個數字,這是因為同一個類裡不可能有兩個同名的成員內部類,而同一個類裡則可能有兩個以上同名的區域性內部類(處於不同方法中),所以 Java 為區域性內部類的 class 檔名中增加了一個數字。

      區域性內部類是一個非常“雞肋”的語法,在實際開發中很少定義區域性內部類,這是因為區域性內部類的作用域太小了:只能在當前方法中使用。大部分時候,定義一個類之後,當然希望多次複用這個類,但區域性內部類無法離開它所在的方法,因此在實際開發中很少使用區域性內部類。

十 . Java 8 改進的匿名內部類

      匿名內部類適合建立那種只需要使用一次的類,建立匿名內部類時會立即建立一個該類的例項,這個類定義立即消失,匿名內部類不能重複使用。定義匿名內部類的格式如下:

new 實現介面 ( ) | 父類構造器 ( 實參列表 )
{

//匿名內部類的實體部分

 }

      從上面定義可以看出,匿名內部類必須繼承一個父類,或實現一個介面,但最多隻能繼承一個父類,或實現一個介面。

      關於匿名內部類還有如下兩條規則:

  • 匿名內部類不能是抽象類,因為系統在建立匿名內部類時,會立即建立匿名內部類的物件。因此不允許將匿名內部類定義成抽象類
  • 匿名內部類不能定義構造器。由於匿名內部類沒有類名,所以無法定義構造器,但匿名內部類可以定義初始化塊,可以通過例項初始化塊來完成構造器需要完成的事情。

      最常見的建立匿名內部類的方式時需要建立某個介面型別的物件,如下程式所示:

interface Student
{
    public String getName();
    public int getAge();
}

public class InnerDemo {
    public void InerTest(Student student)
    {
        System.out.println("該學生的姓名:"+student.getName()+",年齡:"+student.getAge());
}
    public static void main(String [] args)
    {
        InnerDemo innerDemo=new InnerDemo();
//呼叫 InerTest()方法時,需要傳入一個 Student 引數
        //此處傳入匿名實現類的例項
innerDemo.InerTest(new Student() {
            @Override
public String getName() {
                return "Jacky";
}

            @Override
public int getAge() {
                return 20;
}
        });
}
}

     上面程式中的 InnerDemo 定義了一個 InerTest ( ) 方法,該方法需要一個 Student 物件作為引數,但 Student 只是一個介面,無法直接建立物件,因此此處考慮建立一個 Student 介面實現類的物件傳入該方法——如果這個 Student 介面實現類需要重複使用,則應該將該實現類定義成一個獨立類;如果這個 Student 介面實現類只需使用一次,則可採用上面程式中的方式,定義一個匿名內部類。

     定義匿名內部類無須 class 關鍵字,而是在定義匿名內部類時直接生成該匿名內部類的物件。由於匿名內部類不能是抽象類,所以匿名內部類必須實現它的抽象父類或者接口裡包含的所有抽象方法,下面是對於上面程式碼的另外一種實現方式:

interface Student
{
    public String getName();
    public int getAge();
}

class InnerStudent implements Student
{

    @Override
public String getName() {
        return "Jacky";
}

    @Override
public int getAge() {
        return 20;
}
}

public class InnerDemo {
    public void InerTest(Student student)
    {
        System.out.println("該學生的姓名:"+student.getName()+",年齡:"+student.getAge());
}
    public static void main(String [] args)
    {
        InnerDemo innerDemo=new InnerDemo();
innerDemo.InerTest(new InnerStudent());
}
}

      上面兩段程式碼功能完全一樣,只不過採用匿名內部類的寫法更加簡潔。

      當通過實現介面來建立匿名內部類時,匿名內部類也不能顯式建立構造器,因此匿名內部類只有一個隱式的無引數構造器,故 new 介面後的括號裡不能穿入引數。但如果通過繼承父親來建立匿名內部類時,匿名內部類講擁有和父類相似的構造器,此處的相似是指擁有相同的形成列表。看下面的程式:

abstract  class Student
{
    private String name;
    public abstract int getAge();
    public Student(){}
    public Student(String name)
    {
        this.name=name;
}

    public String getName() {
        return name;
}

    public void setName(String name) {
        this.name = name;
}
}

public class InnerDemo {
    public void InerTest(Student student)
    {
        System.out.println("該學生的姓名:"+student.getName()+",年齡:"+student.getAge());
}
    public static void main(String [] args)
    {
        InnerDemo innerDemo=new InnerDemo();
//呼叫有引數的構造器建立 Student 匿名實現類的物件
innerDemo.InerTest(new Student("張三") {
            @Override
public int getAge() {
                return 30;
}
        });
//呼叫無引數的構造器建立 Student 匿名實現類的物件
Student student=new Student() {
            //初始化塊
{
                System.out.println("匿名內部類的初始化塊……");
}
            //實現抽象方法
@Override
public int getAge() {
                return 22;
}
            //重寫父類的例項方法
@Override
public String getName() {
                return "李四";
}
        };
innerDemo.InerTest(student);
}
}

     上面程式建立了一個抽象父類 Student 類,這個抽象父類裡面包含兩個構造器:一個無引數的,一個有引數的。當建立以 Student 為撫慰的匿名內部類時,既可以傳入引數,代表呼叫父類帶引數的構造器;也可以不傳入引數,代表呼叫父類無引數的構造器。當建立匿名內部類時,必須實現介面或抽象父類裡的所有抽象方法。如果有需要,也可以重寫父類中的普通方法。如上面程式中,匿名內部類重寫了抽象父類 Student 類的 getName ( ) 方法,其中 getName ( ) 方法並不是抽象方法。

      在 Java 8 之前,Java 要求被區域性內部類、匿名內部類訪問的區域性變數必須使用 final 修飾,從 Java 8 開始這個限制被取消了,Java 8 更加智慧:如果區域性變數被匿名內部類訪問,那麼該區域性變數相當於自動使用了 final 修飾。

interface Student
{
    void getAge();
}

public class InnerDemo {
    
    public static void main(String [] args)
    {
         int age=10;    //1⃣️
Student student=new Student() {
          @Override
public void getAge() {
              //在 Java 8 以前下面語句將提示錯誤,age 必須使用 final 修飾
              //從 java 8 開始,匿名內部類、區域性內部類允許訪問非 final 的區域性變數
System.out.println(age);
}
      };
student.getAge();
}
}

      Java 8 將這種功能稱為“ effectivity final ”  ,它的意思是對於被匿名內部類訪問的區域性變數,可以用 final 修飾,也可以不用 final 修飾,但必須按照油 final 修飾的方式來用——也就是一次賦值後,以後不能重新賦值。如上面程式如果在1⃣️程式碼後新增如下程式碼:age=20;將會導致編譯錯誤。