Java面向對象系列[內部類]

內部類的概念和定義語法

面向對象類是最小的程序單元,但在某些情況下,也會把一個類放在另一個類的內部來定義,這個定義在其他類內部的類就被稱爲內部類也叫嵌套類,包含內部類的類稱爲外部類也叫宿主類,主要作用如下:

  • 內部類提供了更好的封裝,可以把內部類隱藏在外部類之內,不允許同一個包中的其他類訪問該類
  • 內部類成員可以直接訪問外部類的私有數據,因爲內部類被當成其外部類的成員,同一個類的成員之間可以互相訪問,但外部類不能訪問內部類的實現細節,例如內部類的成員變量
  • 匿名內部類適合用於創建哪些僅需要一次使用的類
  1. 定義內部類與定義外部類的語法大致相同,除了內部類需要定義在其他類裏面之外,內部類比外部類可以多使用3個修飾符:private、protected、static,外部類不可以使用這三個修飾符
  2. 非靜態內部類不能擁有靜態成員

非靜態內部類和靜態內部類

內部類定義很簡單,只需要將類放在另一個類內部定義即可,此處的內部類包括類中的任何位置,甚至在方法中也可以定義內部類(方法裏定義的內部類稱爲局部內部類)

public class OuterClass
{
    控制符 calss innerClass
}

通常內部類被作爲成員內部類定義,而不是作爲局部內部類,成員內部類是一種與成員變量、方法、構造器和初始化塊相似的類成員,局部內部類和匿名內部類則不是類成員。
成員內部分爲兩種:靜態內部類和非靜態內部類,使用static修飾的成員內部類是靜態內部類,沒有使用static修飾的成員內部類非靜態內部類

  • 外部類的上一級程序單元是package,所以它有兩個作用域,同一個包內和任何位置,即package訪問權限和公開訪問權限,正好對應default訪問控制符和public訪問控制符
  • 內部類的上一級程序單元室外部類,它具有4個作用域:同一個類、同一個包、父子類和任何位置,因此可以使用4中訪問控制權限
public class Cow
{
    private double weight;
    // 外部類的兩個重載的構造器
    public Cow(){}
    public Cow(double weight)
    {
        this.weight = weight;
    }
    // 定義一個非靜態內部類
    private class CowLeg
    {
        // 非靜態內部類的兩個實例變量
        private double length;
        private String color;
        // 非靜態內部類的兩個重載的構造器
        public CowLeg(){}
        public CowLeg(double length, String color)
        {
            this.length = length;
            this.color = color;
        }
        public void setLength(double length)
        {
            this.length = length;
        }
        public double getLength()
        {
            return this.length;
        }
        public void setColor(String color)
        {
            this.color = color;
        }
        public String getColor()
        {
            return this.color;
        }
        // 非靜態內部類的實例方法
        public void info()
        {
            System.out.println("當前牛腿顏色是:" + color + ", 高:" + length);
            // 直接訪問外部類的private修飾的成員變量
            System.out.println("本牛腿所在奶牛重:" + weight);   // ①
        }
    }
    public void test()
    {
        var cl = new CowLeg(1.12, "黑白相間");
        cl.info();
    }
    public static void main(String[] args)
    {
        var cow = new Cow(378.9);
        cow.test();
    }
}

編譯的時候會生成兩個class文件,一個是Cow.class,另一個是Cow$CowLeg.class,前者是外部類Cow的class文件,後者是內部類CowLeg的class文件,即成員內部類(包括靜態內部類和非靜態內部類)的class文件總是這種形式:OuterClass$InnerClass.class
在非靜態內部類裏可以直接訪問外部類的private成員,System.out.println("本牛腿所在奶牛重:" + weight);這行代碼就是在CowLeg類的方法內直接訪問其外部類的private實例變量
因爲在非靜態內部類對象裏,保存了一個它所寄生的外部類對象的應用(當調用非靜態內部類的實例方法時,必須有一個非靜態內部類實例,非靜態內部類實例必須寄生在外部類實例裏)
image.png
當在非靜態內部類的方法內訪問某個變量時,系統優先在該方法內查找是否存在該名字的局部變量,如果存在就使用該變量,如果不存在,則到該方法所在的內部類中查找是否存在該名字的成員變量,如果存在則使用該成員變量,如果不存在,則到該內部類所在的外部類中查找是否存在該名字的成員變量,如果存在則使用該成員變量,如果依然不存在,系統報異常。
因此如果外部類成員變量,內部類成員變量與內部類裏方法的局部變量同名,則可通過使用this、外部類類名.this作爲限定來區分

public class DiscernVariable
{
    private String prop = "外部類的實例變量";
    private class InClass
    {
        private String prop = "內部類的實例變量";
        public void info()
        {
            var prop = "局部變量";
            // 通過 外部類類名.this.varName 訪問外部類實例變量
            System.out.println("外部類的實例變量值:" + DiscernVariable.this.prop);
            // 通過 this.varName 訪問內部類實例的變量
            System.out.println("內部類的實例變量值:" + this.prop);
            // 直接訪問局部變量
            System.out.println("局部變量的值:" + prop);
        }
    }
    public void test()
    {
        var in = new InClass();
        in.info();
    }
    public static void main(String[] args)
    {
        new DiscernVariable().test();
    }
}

非靜態內部類的成員可以訪問外部類的private成員,但反過來就不行,非靜態內部類的成員只在非靜態內部類範圍內是可知的,並不能被外部類直接使用
如果外部類需要訪問非靜態內部類的成員,則必須顯示創建非靜態內部類對象來調用訪問其實例成員

public class Outer
{
    private int outProp = 9;
    class Inner
    {
        private int inProp = 5;
        public void accessOuterProp()
        {
            // 非靜態內部類可以直接訪問外部類的private實例變量
            System.out.println("外部類的outProp值:" + outProp);
        }
    }
    public void accessInnerProp()
    {
        // 外部類不能直接訪問非靜態內部類的實例變量,
        // 下面代碼出現編譯錯誤
        System.out.println("內部類的inProp值:" + inProp);
        // 如需訪問內部類的實例變量,必須顯式創建內部類對象
        System.out.println("內部類的inProp值:" + new Inner().inProp);
    }
    public static void main(String[] args)
    {
        // 執行下面代碼,只創建了外部類對象,還未創建內部類對象
        var out = new Outer();     
        out.accessInnerProp();
    }
}

mian方法中創建了一個外部類對象,並調用外部類對象的accessInnerProp()方法,此時非靜態內部類對象根本不存在,如果允許accessInnerProp()方法訪問非靜態內部類對象,則必然報錯

非靜態內部類對象和外部類對象的關係

  • 非靜態內部類對象必須寄生在外部類對象裏,而外部類對象不一定有非靜態內部類對象寄生其中,換句話說如果存在一個非靜態內部類對象,則一定存在一個被他寄生的外部類對象,外部類對象存在時,外部類對象裏不一定寄生了非靜態內部類對象。
  • 外部類對象訪問非靜態內部類成員時,可能非靜態普通內部類對象gen’be根本不存在
  • 非靜態內部類對象訪問外部類成員時,外部類對象一定存在
    同時根據靜態成員不能訪問非靜態成員的規則,外部類的靜態方法、靜態代碼塊不能訪問非靜態內部類,包括不能使用非靜態內部類定義變量、創建實例等,總之不允許外部類的靜態成員直接使用非靜態內部類
public class StaticTest
{
    // 定義一個非靜態的內部類,是一個空類
    private class In{}
    // 外部類的靜態方法
    public static void main(String[] args)
    {
        // 下面代碼引發編譯異常,因爲靜態成員(main()方法)
        // 無法訪問非靜態成員(In類)
        new In();
    }
}

Java不允許在非靜態內部類裏定義靜態成員(靜態方法、靜態成員變量、靜態初始化塊等)

public class InnerNoStatic
{
    private class InnerClass
    {
        /*
        下面三個靜態聲明都將引發如下編譯錯誤:
        非靜態內部類不能有靜態聲明
        */
        static
        {
            System.out.println("=========="); 
        }
        private static int inProp;
        private static void test(){}
    }
}

非靜態內部類不可以有靜態初始化塊,但可以有普通初始化塊

靜態內部類

使用static修飾一個內部類,則這個內部類就屬於外部類本身,而不屬於外部類的某個對象,使用static修飾內部類也可稱爲類內部類
static的作用是把類成員變成類相關,而不是實例相關,就是說static修飾的成員屬於類不屬於單個對象
外部類的上一級程序單元是package,所以不可以使用static修飾,內部類的上一級程序單元是外部類,使用static修飾可以將內部類變成外部類相關,而不是外部類實例相關,即static不可修飾外部類但可修飾內部類
靜態內部類可以包含靜態成員,也可以包含非靜態成員,根據靜態成員不能訪問非靜態成員的規則,靜態內部類不能訪問外部類的實例成員,只能訪問外部類的類成員,即便是靜態內部類的實例方法也不能訪問外部類的實例成員只能訪問外部類的靜態成員

public class StaticInnerClassTest
{
    private int prop1 = 5;
    private static int prop2 = 9;
    static class StaticInnerClass
    {
        // 靜態內部類裏可以包含靜態成員
        private static int age;
        public void accessOuterProp()
        {
            // 下面代碼出現錯誤:
            // 靜態內部類無法訪問外部類的實例變量
            System.out.println(prop1);
            // 下面代碼正常
            System.out.println(prop2);
        }
    }
}

靜態內部類是外部類的類相關的,而不是外部類的對象相關的,靜態內部類對象不是寄生在外部類的實例中,而是寄生在外部類的類本身中。當靜態內部類對象存在時,並不存在一個被他寄生的外部類對象,靜態內部類對象只持有外部類的類引用,沒有外部類對象的引用,如果允許靜態內部類的實例方法訪問外部類的實例成員,但找不到被寄生的外部類對象,則系統會爆異常
靜態內部類是外部類的一個靜態成員,因此外部類的所有方法、所有初始化塊中可以使用靜態內部類來定義變量、創建對象等
外部類依然不能直接訪問靜態內部類的成員,但可以使用靜態內部類的類名作爲調用者來訪問靜態內部類的類成員,也可以使用靜態內部類對象作爲調用者來訪問靜態內部類的實例成員

public class AccessStaticInnerClass
{
    static class StaticInnerClass
    {
        private static int prop1 = 5;
        private int prop2 = 9;
    }
    public void accessInnerProp()
    {
        // System.out.println(prop1);
        // 上面代碼出現錯誤,應改爲如下形式:
        // 通過類名訪問靜態內部類的類成員
        System.out.println(StaticInnerClass.prop1);
        // System.out.println(prop2);
        // 上面代碼出現錯誤,應改爲如下形式:
        // 通過實例訪問靜態內部類的實例成員
        System.out.println(new StaticInnerClass().prop2);
    }
}

內部接口及接口裏的內部類

Java允許在接口裏定義內部類,接口定義的內部類默認public static修飾,也就是說只能是靜態內部類,且接口內部類的訪問控制符必須是public,如果省略則系統默認是public訪問權限。
接口裏的內部接口是接口的成員,系統默認public static修飾,然而接口的目的是定義規範暴露給外界,因此定義接口裏的內部接口沒什麼意義,但語法上是允許的。

在外部類內部使用內部類

  • 在外部類內部使用內部類,與平常使用普通類沒有太大的區別,一樣可以直接通過內部類類名來定義變量,通過new調用內部類構造器創建實例
  • 唯一存在的區別是:不要在外部類的靜態成員(包括靜態方法,靜態初始化塊)中使用非靜態內部類,因爲靜態成員不能訪問非靜態成員
  • 外部類中定義內部類的子類與平常定義子類也無大差別

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

如果希望在外部類以外的地方訪問內部類(包括靜態和非靜態),則內部類不能使用private訪問控制權限,private修飾的內部類只能在外部類內部使用。
對於使用其他訪問控制符修飾的內部類,則能在訪問控制符對應的訪問權限內使用:

  • 省略訪問控制符,只能被與外部類處於同一個package中的其他類所訪問
  • protected修飾的內部類,可以被與外部類處於同一個package中的其他類和外部類的子類所訪問
  • public修飾的內部類,可以在任何地方被訪問
    在外部類以外的地方定義內部類的(靜態和非靜態)變量,語法如下:
    OuterClass.InnerClass varName
    可以看出的是,在外部類以外的地方使用內部類的時候,內部類的類名應該是OuterClass.InnerClass,如果外部類還有package名,則還應該增加package名前綴
    由於非靜態內部類對象必須寄生在外部類的對象裏,因此創建非靜態內部類對象之前,必須先創建外部類對象,在外部類以外的地方創建非靜態內部類實例,語法如下:
    OuterInstance.new InnerConstructor()
    在外部類以外的地方創建非靜態內部類對象必須使用外部類實例和new來調用非靜態內部類的構造器。
class Out
{
    // 定義一個內部類,不使用訪問控制符,
    // 即只有同一個包中其他類可訪問該內部類
    class In
    {
        public In(String msg)
        {
            System.out.println(msg);
        }
    }
}
public class CreateInnerInstance
{
    public static void main(String[] args)
    {
        Out.In in = new Out().new In("測試信息");
        /*
        上面代碼可改爲如下三行代碼:
        使用OuterClass.InnerClass的形式定義內部類變量
        Out.In in;
        創建外部類實例,非靜態內部類實例將寄存在該實例中
        Out out = new Out();
        通過外部類實例和new來調用內部類構造器創建非靜態內部類實例
        in = out.new In("測試信息");
        */
    }
}

當創建一個子類的時候,子類構造器總會調用父類的構造器,因此在創建非靜態內部類的子類時,必須保證讓子類構造器可以調用非靜態內部類的構造器,調用非靜態內部類的構造器時則必須存一個外部類對象

public class SubClass extends Out.In
{
    //顯示定義SubClass的構造器
    public SubClass(Out out)
    {
        //通過傳入的Out對象顯示調用In的構造器,super代表調用In類的構造器
        out.super("hello");
    }
}

如果需要創建SubClass對象時,必須先創建一個Out對象,因爲SubClass是非靜態內部類In類的子類,非靜態內部類In對象裏必須有一個對Out對象的引用,其子類SubClass對象裏也應該持有對Out對象的引用,當創建SubClass對象時傳給該構造器的Out對象就是SubClass對象裏Out對象引用所指向的對象
非靜態內部類In對象和SubClass對象都必須持有指向Out對象的引用,區別是創建兩種對象時傳入Out對象方式不同,創建In的對象時必須通過Out對象來調用new關鍵字;創建SubClass類的對象時,必須使用Out對象作爲調用者來調用In類的構造器。

非靜態內部類的子類不一定是內部類,可以是一個外部類,但非靜態內部類的子類實例一樣需要保留一個引用,該引用指向其父類所在外部類的對象,也就是說如果有一個內部類子類的對象存在,則一定存在與之對應的外部類對象。

在外部類意外使用靜態內部類

因爲靜態內部類是外部類類相關的,因此創建靜態內部類對象時無需創建外部類對象,在外部類以外的地方創建靜態內部類實例

new OuterClass.InnerConstructor()
class StaticOut
{
    // 定義一個靜態內部類,不使用訪問控制符,
    // 即同一個包中其他類可訪問該內部類
    static class StaticIn
    {
        public StaticIn()
        {
            System.out.println("靜態內部類的構造器");
        }
    }
}
public class CreateStaticInnerInstance
{
    public static void main(String[] args)
    {
        StaticOut.StaticIn in = new StaticOut.StaticIn();
        /*
        上面代碼可改爲如下兩行代碼:
        使用OuterClass.InnerClass的形式定義內部類變量
        StaticOut.StaticIn in;
        通過new來調用內部類構造器創建靜態內部類實例
        in = new StaticOut.StaticIn();
        */
    }
}

無論是靜態內部類還是非靜態內部類,他們聲明變量的語法完全一樣,區別只是在創建內部類對象時,靜態內部類只需要使用外部類即可調用構造器,非靜態內部類必須使用外部類對象來調用構造器

創建靜態內部類子類語法

public class StaticSubClass extends StaticOut.StaticIn {}
子類中的內部類和父類中的內部類不可能完全重名,因爲他們的名字都帶上了外部類名,也就不可能重寫父類的內部類

匿名內部類

匿名內部類適合創建那種只需要一次使用的類,創建匿名內部類時會立即創建一個該類的實例,這個類定義立即消失,匿名內部類不能夠重複使用

new 實現接口() | 父類構造器(實參列表)
{
    // 匿名內部類的類體
}
  • 匿名內部類必須繼承一個父類,或者實現一個接口,但最多隻能繼承一個父類或實現一個 接口
  • 匿名內部類不能是抽象類,因爲系統在創建匿名內部類時,會立即創建匿名內部類的對象,因此不允許匿名內部類定義爲抽象類
  • 匿名內部類不能定義構造器,它沒有類名無法定義構造器,但可以定義初始化塊,可以通過實例初始化塊來完成構造器需要完成的事
interface Product
{
    double getPrice();
    String getName();
}
public class AnonymousTest
{
    public void test(Product p)
    {
        System.out.println("His name is" + p.getName() + ",His price is" + p.getPrice());
    }
    public static void main(String[] args)
    {
        var ta = new AnonymousTest();
        // 調用test()方法時,需要傳入一個Product參數,
        // 此處傳入其匿名實現類的實例
        ta.test(new Product()
        {
            public double getPrice()
            {
                return 567.8;
            }
            public String getName()
            {
                return "davieyang";
            }
        });
    }
}

test方法需要個Product對象作爲參數,而Product是個接口,無法直接創建對象,因此需要實現Product接口,如果實現這個接口的類需要重複使用,則應該定義一個獨立的類,如果只是一次性使用則可以用匿名內部類
匿名內部類不能是抽象類,因此它必須實現它的抽象父類或者接口裏的所有抽象方法,上邊的匿名內部類的還可以寫成:

class AnonymousProduct implements Product
{
    public double getPrice()
    {
        return 567.8;
    }
    public String getName()
    {
        return "davieyang"
    }
}
ta.test(new AnonymousProduct());

顯然匿名構造類的寫法更加簡單

  • 當通過實現接口來創建匿名內部類時,匿名內部類也不能顯示創建構造器,因此匿名內部類只有一個隱式的無參數構造器,因此new接口名後的括號裏不能傳入參數值
  • 如果通過繼承父類來創建匿名內部類時,匿名內部類將擁有和父類相似的構造器(擁有相同的形參列表)
abstract class Device
{
    private String name;
    public abstract double getPrice();
    public Device(){}
    public Device(String name)
    {
        this.name = name;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }
}
public class AnonymousInner
{
    public void test(Device d)
    {
        System.out.println("購買了一個" + d.getName() + ",花掉了" + d.getPrice());
    }
    public static void main(String[] args)
    {
        var ai = new AnonymousInner();
        // 調用有參數的構造器創建Device匿名實現類的對象
        ai.test(new Device("電子示波器")
        {
            public double getPrice()
            {
                return 67.8;
            }
        });
        // 調用無參數的構造器創建Device匿名實現類的對象
        var d = new Device()
        {
            // 初始化塊
            {
                System.out.println("匿名內部類的初始化塊...");
            }
            // 實現抽象方法
            public double getPrice()
            {
                return 56.2;
            }
            // 重寫父類的實例方法
            public String getName()
            {
                return "鍵盤";
            }
        };
        ai.test(d);
    }
}

創建匿名內部類時,必須實現接口或抽象父類中的所有抽象方法,如果有必要還可以重寫父類中的普通方法
在Java8之前,要求被局部內部類、匿名內部類訪問的局部變量必須使用final修飾,Java8之後無此限制而是如果局部變量貝寧名內部類訪問,那麼局部變量相當於自動使用了final修飾

interface A
{
    void test();
}
public class ATest
{
    public static void main(String[] args)
    {
        int age = 8;     // ①
        // 下面代碼將會導致編譯錯誤
        // 由於age局部變量被匿名內部類訪問了,因此age相當於被final修飾了
        // age = 2;
        var a = new A()
        {
            public void test()
            {
                // 在Java 8以前下面語句將提示錯誤:age必須使用final修飾
                // 從Java 8開始,匿名內部類、局部內部類允許訪問非final的局部變量
                System.out.println(age);
            }
        };
        a.test();
    }
}

effectively final,其意思是對於被匿名內部類訪問的局部變量,可以用final修飾,也可以不用final修飾,但必須按照有final修飾的方式來用,即一次賦值後不能再次賦值。
局部內部類
如果把一個內部類放在一個方法裏定義,則這個內部類就是一個局部內部類,局部內部類僅在該方法裏有效,且不能使用訪問控制符和static修飾,因爲他們的上一級程序單元是方法,而不是類因此不管是局部變量還是局部內部類使用static都毫無意義,所有局部成員都不能使用static,並且局部成員的作用域是所在方法,其他程序單元永遠無法訪問到,因此使用訪問控制符也無意義

如果需要用局部內部類定義變量、創建實例或派生子類都只能在局部內部類所在的方法內進行

public class LocalInnerClass
{
    public static void main(String[] args)
    {
        // 定義局部內部類
        class InnerBase
        {
            int a;
        }
        // 定義局部內部類的子類
        class InnerSub extends InnerBase
        {
            int b;
        }
        // 創建局部內部類的對象
        var is = new InnerSub();
        is.a = 5;
        is.b = 8;
        System.out.println("InnerSub對象的a和b實例變量是:" + is.a + "," + is.b);
    }
}

編譯這個文件會生成3個class文件,LocalInnerClass.class/LocalInnerClass$1InnerBase.class/LocalInnerClass$1InnerSub.class
他們遵循的規則是OuterClass$NInnerClass.class局部內部類的class文件名比成員內部類的class文件名多了個數字,這是因爲同一個類裏不可能有兩個同名的成員內部類,同一個類裏則可能有兩個以上同名的局部內部類分散在不同的方法中而已,所以增加個數字用於區分
局部內部類是個雞肋,創建類是爲了複用,而局部內部類太扯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章