Java 的各種內部類、Lambda表示式

Life_Goes_On 發表於 2020-09-13

內部類

內部類是指在一個外部類的內部再定義一個類。內部類的出現,再次打破了Java單繼承的侷限性。

內部類可以是靜態 static 的,也可用 public,default,protected 和 private 修飾。(而外部頂級類即類名和檔名相同的只能使用 public 和 default)。

注意:內部類是一個編譯時的概念,一旦編譯成功,就會成為完全不同的兩個類。

對於一個名為outer的外部類和其內部定義的名為inner的內部類。編譯完成後出現 outer.class 和outer$inner.class 兩類。所以內部類的成員變數/方法名是可以和外部類的相同的。


1 成員內部類


也就是普通的內部類,它在外部類裡面一層。

  • 成員內部類可以訪問外部類的所有成員和方法;

  • 外部類要訪問成員內部類的成員和方法需要通過例項物件來訪問;

  • 成員內部類不能含有 static 的變數和方法

    想一想,非static的內部類,在外部類載入的時候,並不會載入它;可是你這個內部類卻擁有 static 的變數和方法,這是必須先載入的,就會和這個內部類的載入時機衝突

    事實上,這種寫法編譯器就能提示錯誤:

Java 的各種內部類、Lambda表示式

具體的使用方法,參考如下程式碼及註釋:

public class Outer{
    private int outi = 0;//外部類的成員
    //外部類的方法
    public void outPrint(){
        System.out.println("out");
    }
    //成員內部類
    class Inner{
        int ini = outi + 1;//內部類可以訪問外部類的成員變數
        public void inPrint(){
            outPrint();//內部類可以訪問外部類的成員方法
            System.out.println("in");
        }
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        //這裡因為main是靜態方法,訪問非靜態的 Inner物件需要一個Outer物件
        Inner inner = outer.new Inner();
        inner.inPrint();//外部類訪問內部類的成員方法
        System.out.println(inner.ini);//外部類訪問內部類的成員變數
    }
}

2 靜態內部類


解決普通的成員內部類不能含有 static 成員的問題,就是將成員內部類宣告為 static

可以這麼說:靜態內部類只能訪問其外圍類的靜態成員,除此之外與非靜態內部類沒有任何區別。

靜態內部類相當於一個邏輯上可以獨立出去的類,不過放在了內部而已。

  • 靜態內部類不依賴於外部類的載入。(根據是否使用去載入,相當於內外平行的關係。)
  • 靜態內部類不能直接訪問外部類的非靜態成員。(因為外部類載入的時候非靜態成員是沒有載入的,除非例項化之後)

對比普通成員內部類的程式碼如下:

public class Outer{
    private int outi = 0;//外部類的成員
    //外部類的方法
    public void outPrint(){
        System.out.println("out");
    }
    //靜態成員內部類
    static class Inner{
        Outer outer = new Outer();
        int ini = outer.outi+1;//只能通過例項
        //int ini = outi + 1;//不能訪問外部類的普通成員變數
        public void inPrint(){
            outer.outPrint();//只能通過例項
            //outPrint();//不能訪問外部類的普通成員方法
            System.out.println("in");
        }
    }

    public static void main(String[] args) {
        Inner inner = new Inner();//可以直接new出來,因為相當於是兩個獨立的類
        inner.inPrint();
        System.out.println(inner.ini);
    }
}

對於靜態內部類來說,我們的感受會更加明顯,既然兩個類都沒什麼關係了,為什麼還要放在內部作為一個內部類呢?

其實就是為了某一些層級關係,當 Inner 的使用範圍很小,適用於包含在 Outer 類當中,但又不依賴於外在的類,就這麼寫,同時還能夠讓程式碼的類間層級關係更加清晰,而不用重新放在另一個檔案。


3 區域性內部類


區域性內部類,是指內部類定義在方法內部,或者某一個作用域裡的類。

  • 區域性內部類只能在方法裡面例項化,外面就不行了;
  • 區域性內部類訪問外部方法的變數,這個變數必須有 final 修飾。

例如下面的程式碼:

class Outter{ 
    public void outMethod(){ 
        final int i=0; 
        class Inner{ 
            //使用i 
        } 
 
        Inner in=new Inner(); 
    } 
} 

很明顯,和普通的成員內部類是一樣的定義,但是這裡強調的特殊點:i 為什麼必須是 final 的呢?

原因是:

  • 當 JVM 執行到需要建立 Inner 物件之後,Outter 類已經全部執行完畢,那麼垃圾回收機制是有可能釋放掉區域性變數 i 的,而如果按照普通的成員內部類的定義,i 應該可以訪問才對,那這個問題需要一個約束,所以編譯器解決了這個問題,他會在 Inner 類中建立了一個 i 的備份;
  • 那麼就會有新的問題,外部類的 i 變化的時候,內部類的 i 要一致才行;
  • 因此不得不規定死這些區域性域必須是常量,必須用 final 修飾,保證資料的一致。

4 匿名內部類


4.1 匿名內部類

當一個區域性內部類沒有名字,這就是匿名內部類。

  • 實現的方式是建立一個帶內容的外部類、或者介面的子類匿名物件
  • 和普通的區域性內部類又不同,訪問外部方法的變數,這個變數可以不是final修飾。(jdk1.8之後)

比如我們常見的兩種

    public static void main(String[] args) {
        //第一種方式
        new Thread(){
            @Override
            public void run() {
                System.out.println("內部類輸出……");
            }
        }.start();
        //第二種方式
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("內部類輸出……");
            }
        }).start();
    }
  • 第一種方式就是,建立了一個帶內容的外部類,重寫了它的方法,但是這個類我們沒有起名字,而是直接建立了一個例項;
  • 第二種方式就是通過實現一個介面,實現對應的方法,同樣,這個實現類我們也沒有起名字,而是直接建立了一個例項。

正是因為上面所說的,匿名內部類可以訪問外部的變數,而且不用是 final ,同理也可以持有外部類的引用,這種情況都可能會導致記憶體洩漏問題。

外部類的生命週期到了,但是卻因為內部類持有者外部類的引用,所以導致外部類無法被回收,造成記憶體洩漏。

解決方案就是:

  • 使用靜態內部類,並且不要持有外部類的引用,如果要呼叫外部類方法或使用外部類屬性,可以使用弱引用(前面說過,靜態內部類是和外部類平行的,弱引用會安全)。

4.2 匿名內部類和 lambda 表示式

從 java 8 開始,引入了 Lambda 表示式,將程式碼塊作為引數使用更簡潔的程式碼來建立只有一個抽象方法的介面(這種介面被稱為函式式介面)的例項

仍然用上面的匿名內部類的程式碼作為示例:

    public static void main(String[] args) {
		new Thread(()-> System.out.println("內部類輸出")).start();
    }

可以看出,lambda 表示式代替匿名內部類的時候,lambda 程式碼塊寫的是代替實現抽象類的方法體,總結一下lambda表示式的語法主要由三部分構成:

  • 形參列表,如果只有一個引數可以省略括號,當無引數型別時可以使用()或者obj來代替。

  • 箭頭 ->

  • 程式碼塊部分,如果程式碼只有一行則可以省略掉花括號,不然使用花括號將lambda表示式的程式碼部分標記出來。

寫法方面,我們再自己定義一個介面,然後寫一下 lambda 式有引數的情況:

//自定義介面
interface Origin{
    int sum(int a, int b);//待實現方法,有引數
}

public class LambdaTest {
    public static void main(String[] args) {
        //寫法1:使用lambda表示式實現介面
        Origin o = (int a, int b)-> {
            return a+b;
        };
        
        //寫法2:省略引數型別
        Origin o1 = (a, b)->{
            return a+b;
        };
        
        //寫法3,省略花括號(只適用於方法實現只有一行的情況)
        Origin o2 = (a, b)-> a+b;
        
        System.out.println(o.sum(100,100));
    }
}

4.3 java 的四種引用型別

  1. 強引用

    最常見的普通物件引用,只要還有強引用指向一個物件,說明那個物件還活著,垃圾回收不會回收這種物件;

  2. 弱引用

    垃圾回收器一旦發現具有弱引用的物件,不管當前記憶體空間是否足夠,都會回收他的記憶體。(即使弱引用被其他強引用引用,還是會被回收)

  3. 軟引用

    如果一個物件只具備軟引用,如果記憶體空間足夠,那麼 JVM 就不會 GC 它,如果記憶體空間不足了,就會GC該物件

  4. 虛引用

    如果一個物件只具有虛引用,那麼它就和沒有任何引用一樣,隨時會被JVM當作垃圾進行GC。

不會被回收的情況?

強引用,只要還存在強引用就不會被回收。

關於強引用和弱引用帶來的問題, 最明顯的就在 ThreadLocal 的使用上。