1. 程式人生 > 遊戲資訊 >艾爾登法環笑話合集(三)

艾爾登法環笑話合集(三)

Java內部類

Java中可以將一個類定義在另一個類中或一個方法中,這樣的類稱為內部類

內部類一般來說分為下面幾種:

  • 成員內部類(可以分為靜態成員內部類、非靜態成員內部類)
  • 區域性內部類
  • 匿名內部類

一、成員內部類

成員內部類看起來像是外部類的一個成員,可以使用private、public等訪問限制符修飾。也可以使用static修飾。根據是否使用static,成員內部類分為:

  • 靜態成員內部類:使用了static進行修飾
  • 非靜態成員內部類:未使用static進行修飾

除了靜態內部類以外,所有內部類在編譯完成後隱含地儲存著一個外圍類的物件的引用--《Java核心技術卷一》

1 靜態內部類

使用static修飾的內部類稱為靜態內部類。並且只有內部類才能被宣告為static,而外部類不可以。

由於靜態內部類編譯完成後沒有外圍類的物件的引用,意味著:

  • 靜態內部類不依賴於外圍類的物件
  • 靜態內部類不能使用外圍類的非static成員和方法
  • 靜態內部類允許有static屬性、方法

2 非靜態內部類

由於非靜態內部類在編譯完成後隱含地儲存著一個外圍類的物件的引用,意味著:

  • 非靜態內部類可以訪問外部類的所有資訊(如果有重名,使用外部類.this.變數/方法的方式來呼叫外部類的變數或方法)
  • 建立內部類物件時,必須先使用外圍類的物件來建立
  • 外圍類可以訪問成員內部類資訊,但必須先建立一個內部類的物件,再使用這個物件來訪問
  • 非靜態內部類不能存在static的變數和方法,可以存在某一些static final型別的常量(具體指的是編譯期常量)

3 內部類語法

對於靜態內部類之外的所有內部類:

  1. 儲存了外部類的引用,在內部類使用外圍類的語法為:
OutClass.this

表示外圍類的引用,如果我們在外圍類和內部類定義了相同名稱的變數,如num,在內部類中就可以使用他來區分

OutClass.this.num;	//表示外圍類中定義的num
this.num;			//表示內部類定義的num
  1. 在外圍類的作用域之外使用內部類:
OutClass.InnerClass
  1. 內部類的構造需要用到外部類物件
OutClass.InnerClass InnerObject = OutObject.new InnerClass();

4 為什麼非靜態內部類不能存在static的變數和方法

這是Java的一個語法規則,為什麼會制定這樣的規則呢,分析可能的原因

4.1 常量池

常量池用來儲存一些資料,這些資料在編譯時期就已經被確定,並且被儲存在已編譯好的.class類檔案中。

常量池中的資料主要有兩大類:

  • 字面量:Java語言層面的常量,如程式中定義的各種基本型別資料、物件型資料(如String類物件和陣列)
  • 符號引用:編譯原理層面的常量,包含:類和介面的全限定名、欄位名稱和描述符、方法名稱和描述符

4.2 關於final

  1. final可以修飾:屬性、方法、類、區域性變數
  2. 用final修飾的變量表示常量,一旦初始化就不可更改,對於基本型別不可更改其數值,對於引用型別不可指向其他的物件
  3. final可以修飾的變數有三種:靜態變數、例項變數、區域性變數
  4. final修飾的屬性的初始化有的發生在編譯期(編譯期常量),有的發生在執行期(執行期常量)
  5. final修飾的方法表示該方法在子類中不可重寫
  6. final修飾的類不可被繼承
  • 編譯期常量

    如果在編譯時,final變數是基本型別或String型別,且jvm可以確定它的確切值,那麼編譯器會把它當做編譯期常量使用,使用字面量替換並存入class常量池,在需要它的時候,直接訪問這個常量

  • 執行時常量

    即並不直接用字面量為final常量賦值,中間經過引用或獲取的過程(可以是對區域性變數的獲取或某些處理過程結果的獲取),在程式的編譯期並不關注其本身的值,只是知道型別即可,在執行階段才對其值進行確定,而編譯期常量在編譯期中直接被替換為字面量,寫入class常量池中

4.3 對類的依賴

如果在程式中分別呼叫編譯期常量/執行時常量,則

  • 編譯期常量在編譯階段就存放進了class常量池,使用它不會引起類的載入
  • 執行時常量依賴類,會引起類的初始化(類載入)

4.4 問題分析

public class OutClass {
	//非靜態內部類
	public class InnerClass {
		public static int i = 1;					//錯誤
		public static final int j = 1;				//正確
		public static final Date d = new Date();	//錯誤
		
	}
	
	public static void main(String[] args)
	{
		System.out.println(OutClass.InnerClass.i);
		System.out.println(OutClass.InnerClass.j);
		System.out.println(OutClass.InnerClass.d);
	}
}

Java中變數的初始化順序為:

(靜態變數、靜態初始化塊)-->(變數、初始化塊)--> 構造器

在執行上面的程式碼時,JVM先載入外部類OutClass,然後執行靜態變數、靜態初始塊的初始化,再載入非靜態程式碼塊。此時內部類InnerClass好像是要被載入了,但是實際上並沒有。因為非靜態內部類需要有外部類的物件的引用,所以非靜態內部類的載入必須要等到外部類例項化之後,只有建立了一個外部類物件,JVM才能載入其內部類的位元組碼。又因為static變數的初始化需要載入位元組碼,所以此時 i 並未被初始化,d 為執行時常量,所以此時 d 也未被初始化。因此此時這樣使用內部類的變數是錯誤的,為了避免這種錯誤,Java才規定不能在內部類使用靜態變數執行時常量

5 應用

  1. 使用內部類定義複雜的資料結構
  2. 定義常量
  3. 靜態內部內實現單例
public class SingleTon{
	private SingleTon(){}

    private static class SingleTonHoler{
        private static SingleTon INSTANCE = new SingleTon();
	}
 
    public static SingleTon getInstance(){
        return SingleTonHoler.INSTANCE;
	}
}

外部類載入時不需要立即載入內部類,內部類不被載入則不會初始化INSTANCE,這就實現了執行緒安全的懶漢式單例模式

  1. 成員內部類可以實現多繼承

二、區域性內部類

區域性類不能使用public或private訪問說明符來進行宣告。它的作用域被限定在宣告這個區域性類的塊中。區域性類可以外部完全隱藏,只能在被宣告的作用域中使用,即使是宣告程式碼塊所在類的其他方法也不能使用這個內部類。

訪問區域性變數

區域性內部類不僅可以訪問包含他的外部類,還可以訪問區域性變數,但是必須確保區域性變數必須事實上是不可變的(不是必須用final修飾),這一點和lambda類似。

區域性內部類使用區域性變數時,編譯器會檢測對區域性變數的訪問,為每個變數建立相應的資料域,並將區域性變數拷貝到構造器中,以便這些資料與初始化為區域性變數的副本。

為什麼要儲存副本以及為什麼不可變?原因和lambda表示式一樣。

區域性變數的生存期很短,而區域性內部類建立的物件生存期卻可能很長,比如作為引數被傳遞到計時器中等。不論各種情況,如果不儲存一個副本當區域性內部類中使用到區域性變數時,這個區域性變數很有可能已經被回收掉了。由於一個區域性內部類的物件就會生成這樣的一個副本,如果這個變數是可變的,就無法儲存資料同步。因此區域性內部類只能訪問不可變的區域性變數

三、匿名內部類

很多時候我們編寫區域性內部類也只是會它來建立一個物件,並不會重複使用,所以我們連類名都不需要起了。例如

//我們聲明瞭一個類實現了ActionListener介面,並直接建立了一個物件返回給ActionListener型別的引用
ActionListener lister = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("hello");
    }
};

眾所周知,類構造器需要和類名一致,但是匿名內部類連名字都沒有,所以匿名內部類沒有構造器。但是可以將引數傳遞給超類的構造器。但是在實現介面時就不能有任何構造引數了。

//定義了匿名內部類,使用父類的構造器
Person p = new Person("Jack"){...};

匿名內部類的使用技巧

  1. 雙括號初始化

    如果想要構造一個數組列表作為函式的引數,可以使用如下方式:

    //有一個名為fun的函式,需要一個String型別的陣列作為引數
    void fun(List<String> strs){...}
    
    //第一層大括號表示正在定義匿名內部類,第二層大括號為初始程式碼塊
    fun(new ArrayList<String>(){{add("Jack"); add("Tom");}});
    
  2. 列印靜態方法所在的類名

    在生成日誌或者除錯時,通常需要列印當前的類名,如:

    System.out.println("current class is: " + getClass());
    

    但是,這種方式對於靜態方法是無效的,因為靜態方法沒有this,所以應該使用如下的方法獲取當前類:

    //通過建立Object一個匿名子類的物件,再呼叫getEnclosingClass()獲取到外圍類
    new Object(){}.getClass().getEnclosingClass();
    

參考資料

Java static關鍵字詳解

Java類載入機制的七個階段,載入、驗證、準備、解析、初始化、使用、解除安裝

JAVA--final關鍵字、編譯期常量與執行時常量

《為什麼非靜態內部類中不能有static屬性的變數,卻可以有static final屬性的變數?》

Java內部類(一篇就夠)_趕路人兒的部落格-CSDN部落格_java內部類