你真的懂匿名類、lambda、方法引用?先過了這道題再說!!!

先給出一道很簡潔的小段 Java 程序,你看一下是否能答出正確結果。

在類中有一個靜態變量;
靜態方法塊中,拋出子線程修改變量的值,然後等待子線程執行結束;
main 方法查看變量的值。

public class LambdaTest {
    // 靜態變量初始爲false
    static boolean b = false;
    static {
        // 拋出子線程將變量改爲true
        Thread t = new Thread(() -> b = true);
        t.start();
        // 等待子線程執行完成
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // 輸出變量是否被修改
        System.out.println(b);
    }
}

最好不要先偷窺答案。
我們先想一想可能結果是什麼:
比如

  1. 修改成功,打印 true
  2. 修改不成功,打印 false
  3. 編譯錯誤(猜的)
  4. 程序卡死(我也不知道,你自己想)
  5. 程序拋出異常。。

也許還有其他答案吧,需要你們自己分析判斷。

接下來,就是見證奇蹟的時刻:
Lambda
好吧,程序卡死了。

不知道你猜對了沒有。
假設你碰巧猜對了,
那我再對代碼稍作修改,你看一下是否還能繼續猜對

嘗試將 lambda 表達式改爲空

Thread t = new Thread(() -> {});

總代碼

public class LambdaTest {
    // 靜態變量初始爲false
    static boolean b = false;
    static {
        // 這次我什麼都不幹總可以了吧
        Thread t = new Thread(() -> {});
        t.start();
        // 等待子線程執行完成
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        // 輸出變量
        System.out.println(b);
    }
}

lambda
不要懵逼,我再改一改

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("你幹嘛呢");
    }
});

總代碼

public class AnonymousInnerClass {
    static boolean b = false;
    static {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("你幹嘛呢");
            }
        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(b);
    }
}

匿名內部類
不錯,執行完了
再來:

Thread t = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("子線程運行");
        b = true;
    }
});

完整代碼

public class AnonymousInnerClass {
    static boolean b = false;
    static {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程運行");
                b = true;
            }
        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(b);
    }
}

匿名內部類
誒?
子線程運行了?
可是怎麼還是卡死了。。。。

不急,繼續
改成方法引用

// 改成方法引用
Thread t = new Thread(System.out::println);

完整代碼

public class MethodQuote {
    static boolean b = false;
    static {
        Thread t = new Thread(System.out::println);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(b);
    }
}

方法引用
它又奇怪的執行結束了。。。
(當然全答出來的大佬就不要來吐槽我了)

解析

經過百般波折,你們可能已經蒙了。
這其中主要是涉及到

  • 匿名內部類
  • lambda 表達式
  • 方法引用

我們發現,這三種方式,拋出的線程,執行的結果狀態都不一樣。

可能很多人想當然的認爲,lambda 表達式,和匿名內部類是同一種實現,只是寫法不同。
然而實際上不是的(從這裏面也可以體現出)。

下面我們一步步來分析。

爲了方便定位,我把 new 出來的子線程命了一個名字:叫子線程
定位
然後我們打印出線程信息,發現,子線程還是 RUNNABLE 的狀態。
但是,我們仔細一看:
cpu=0.00ms
說明它根本就還沒有執行 !!!
在這裏插入圖片描述
然後我們再轉頭一看主線程,發現它已經陷入了 WAITING 中,
也就是說,我們的程序已經被卡死了。
在這裏插入圖片描述
爲了理清到底是哪些地方出了問題,我們在程序的各個點打印輸出,來進行錯誤排查

public class LambdaTest {
    static {
        System.out.println(Thread.currentThread().getName() + ":static方法開始");
        Thread t = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ":子線程運行");
        }, "子線程");
        t.start();
        try {
            System.out.println(Thread.currentThread().getName() + ":子線程join");
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":static方法結束");
    }
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ":main方法運行");
    }
}

print排查

  • 我們可以發現,類的初始化是由 main 線程執行的。
  • 然後,程序運行到 join 之後,整個程序卡死。
  • 我們之前也看過,main 線程是處於 WAITING 狀態的

所以肯定是運行的子線程哪裏有問題。

我們點開 join 方法,也可以發現,join 的本質也就是 waitjoin
我們 debug 一下,會發現線程在執行結束後,最終會退出,喚醒那些 join 了自己的線程
在這裏插入圖片描述
在這裏插入圖片描述
所以我們就要去弄明白,爲什麼子線程沒有成功執行。
這就涉及到 lambda 表達式 和 匿名內部類 之間的區別了。

我們先看匿名內部類:
我們打開編譯過後的 class 所在的文件夾,打開過後,發現有兩個 class 文件,
一個是我們之前寫的測試類;
還有一個就是我們的匿名內部類,它的名字是在所在的類的名字後面加 $1
如果還有其它匿名內部類的話,就依次是 $1、$2、$3、$4……
匿名內部類
也就是說,我們的匿名內部類,在編譯的時候,就已經存在了。
但是我們的 lambda表達式、方法引用,在編譯的時候不會產生類,而是運行時動態產生的。
(因爲我們編譯過後沒有產生其它 class)
在這裏插入圖片描述
我們目前已經開始發現一點區別了。
一個是靜態生成類,編譯時已經存在;
一個是動態生成類,只有在運行時纔會生成類。

我們繼續探究,
點開編譯後 class 的指令,我們可以發現:
在匿名內部類中,有 invokespecial 字節碼指令。
匿名內部類
我們翻開官方文檔:
上面說明了這是一個初始化的指令。
文檔
其實我們在看到 class 字節碼指令的時候,後面寫了 init,我們也就能想出來了。
官方說明是爲了確認。
init
但是到了我們的 lambda 表達式的時候,我們可以看到:
在 class 字節碼中表示的是 invokedynamic 動態指令
lambda表達式
區別顯而易見,匿名內部類由於編譯時已經產生,所以完全可以直接初始化,從而創建的線程可以正確執行匿名內部類的方法。
匿名內部類
不過我們之前的題目中有一個示例,匿名內部類也無法成功執行,我們來看一下這又是爲什麼:
匿名內部類
剛纔我們探討過了,採用匿名內部類是可以成功執行的,因爲在編譯時,類文件已經產生。

但是這裏,我們發現了一個點,該匿名內部類引用了我們外部類的靜態變量。

那我現在我們來分析程序執行流程。

  1. 首先,程序創建了匿名內部類的對象,然後創建子線程去跑這個 Runnable 對象。
  2. 線程啓動後,開始執行,主線程調用 join 等待子線程結束
  3. 子線程成功打印了 “子線程運行” 這句話(我們之前的執行結果便是如此)
  4. 但是子線程訪問到 外部類的變量

這就是我們要注意的點了,我們要去訪問一個類的時候,這個類必須是初始化完成的 !!!
(按照道理我們不可以去訪問才初始化一半的對象,這時候 static 方法都沒執行完)

所以,子線程此刻便在等待,主線程完成外部類的初始化;
但是,這時候主線程還在等子線程執行完。。。。

所以,就產生了死鎖。

所以,不管是對於匿名內部類,還是其他任意的一個類,只要去訪問其他類,就必須等到那個類完成初始化。
這也就是我們在類中訪問了外部類變量而導致死鎖的原因。

我們繼續回到 lambda 表達式,我們之前已經發現,它在編譯的時候,根本沒有創建出類。
也就是類是運行時生成。

這時候,我們不管有沒有在類的方法中引用外部類的變量,都沒有什麼實質性的作用。
因爲這時候要動態生成內部類,就必須先初始化外部類。
在這裏插入圖片描述
這時候,我們已經區分好了匿名內部類和 lambda 表達式的區別,雖然都是生成類,但是:

  • 匿名內部類是編譯時靜態生成
  • lambda 表達式是運行時動態生成類

但是,我們可別忘了,我們還有一個方法引用沒有講
我們記得當時的代碼示例,在方法引用的時候,程序是可以成功執行結束的。
但是我們點開一看,發現卻是 invokedynamic
在這裏插入圖片描述
怎麼感覺我在啪啪打臉??
不怕,我們繼續點進去看一下。。
我們可以發現,由於 Runnable 引用的就是一個 pringln 方法,
而 System.out 對象,早就以及初始化完成了,所以根本不用去擔心初始化內部類而要先初始化外部類的問題。
在這裏插入圖片描述
因而,採取方法引用的方式,也可以執行成功。

總結

現在,想必你對匿名內部類、lambda 表達式、方法引用,都有一定理解了。

其實,對於這些 Java 的知識,不僅僅只是簡單使用即可。
更多的時候,要去弄明白其中的原由,仍需要對一些源碼的掌握,以及對 Java 虛擬機的理解,操作系統底層的一些瞭解方可。

因爲很多時候,代碼的邏輯是確實不存在問題的,但是,對於一些底層的原理,會或多或少的影響到我們程序的執行。
因此,爲了超越一個 CRUD 程序員,我們必須去對知識的橫向、縱向擴展。

這樣,才能掌握徹底,理解深刻,方能運用自如。

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