Java 中使用 Lambda 為什么只能使用 final 變量?


Java 中使用 Lambda 為什么只能使用 final 變量?

這兩天公司內部有人在討論 Kotlin,說 Kotlin 很好用。甚至還有人說,Kotlin 會取代 Java!

太天真了,如果你說 Go 能取代 Java 我還能信,Kotlin 要是能取代 Java,Oracle 第一個不答應。雖然 Kotlin 和 Java 都寄生於 JVM,但畢竟 Java 才是親兒子。

我個人認為 Kotlin 並不會取代 Java,而是一個以“工具人”的角色存在於 JVM 生態中。

  • Kotlin 沒有大的抱負,僅僅定位為一套工具,它的一切特性都為實用、簡潔而生。

  • Kotlin 不是革命者,而是改良者,它不像 Go,沒有取天下而代之的野心,只有“讓 Java 更好用”的踏實目標,積跬步而至千里。

  • Kotlin 也不完美,但在不斷進步,它不像 Java 被 Oracle 一家把持,不允許任何不受控制的特性出現,Kotlin 的誕生和發展都離不開社區推動,越來越多的新特性正在應開發者呼吁加入其中。

  • Kotlin 不會面面俱到,而是以補 Java 的短板為先,Kotlin 不想取代任何人。

拿 Kotlin 和 Java 進行比較,其實是不公平的。Kotlin 寄生於 JVM,它其中的函數式編程使用體驗好於 Java。Java 中的 Lamdba 對於參數限制為 final,而 Kotlin 則沒有這個限制,究其根本原因是實現原理上的不同。

Java Lambda 表達式

Lambda 表達式,也可稱為閉包,它是推動 Java 8 發布的最重要新特性。

Lambda 允許把函數作為一個方法的參數(函數作為參數傳遞進方法中)。

使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。

Java Lambda 語法

(parameters) -> expression
// 或
(parameters) ->{ statements; }

以下是 lambda 表達式的重要特征:

  • 可選類型聲明:不需要聲明參數類型,編譯器可以統一識別參數值。
  • 可選的參數圓括號:一個參數無需定義圓括號,但多個參數需要定義圓括號。
  • 可選的大括號:如果主體包含了一個語句,就不需要使用大括號。
  • 可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定表達式返回了一個數值。

Lambda 表達式實例

Lambda 表達式的簡單例子:

// 1. 不需要參數,返回值為 5  
() -> 5  
  
// 2. 接收一個參數(數字類型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2個參數(數字),並返回他們的差值  
(x, y) -> x – y  
  
// 4. 接收2個int型整數,返回他們的和  
(int x, int y) -> x + y  
  
// 5. 接受一個 string 對象,並在控制台打印,不返回任何值(看起來像是返回void)  
(String s) -> System.out.print(s)

使用 Lambda 表達式需要注意以下兩點:

  • Lambda 表達式主要用來定義行內執行的方法類型接口,例如,一個簡單方法接口。
  • Lambda 表達式免去了使用匿名方法的麻煩,並且給予 Java 簡單但是強大的函數化的編程能力。

Lambda 原理

很多人提到 Lambda 的原因,就直接說 Lambda 是靠匿名內部類實現的。這個說法不完全准確。

Lambda 表達式,有可能會生成內部類;也有可能會生成私有靜態方法,還有可能生成私有方法。

具體是哪種形式,和你使用的函數式編程有關。

關於這個原理,我認為可以單獨拿一篇文章來說,今天不過多討論。

Lambda 變量作用域

lambda 表達式只能引用標記了 final 的外層局部變量,這就是說不能在 lambda 內部修改定義在域外的局部變量,否則會編譯錯誤。

Object instanceObj = new Object();

private void testLambda() {
    // 用於直接引用
    Object localObj1 = new Object();
    // 用於傳參
    Object localObj2 = new Object();
    System.out.println(Thread.currentThread().getName());
    int num = 10;
    Consumer consumer = (x) -> {
        System.out.println(x);
        System.out.println(localObj1);
        System.out.println(instanceObj);
        System.out.println(num);
        System.out.println("consumer:" + Thread.currentThread().getName());
    };
    consumer.accept(localObj2);

}

上面代碼中有一個 num 變量,並沒有標記為 final。但是它卻被 Lambda 表達式使用了。所以,是你說的不對?

我說的並沒有錯,原因是,在 Java 中:如果我聲明了一個變量,且在后面不更改它的值,那么那就是事實上的 final。這種變量在 lambda 是可以使用的,但是不能被修改。

如果我們嘗試修改 num 變量,發現不被允許。

圖片
Lambda final

如果我們不在 Lambda 中修改它,而只在 Lambda 中使用它。然后,在 Lambda 外部修改它,可能會有並發問題。正常情況下是允許的,但是在線程中是不被允許的。

public void test(){
    OpTest opTest = (x, y) -> 10 + 20 + x + y;
    int a = 10, b = 20;

    System.out.println(opTest.opTest(a, b));

    a = 0;
    b = 0;
    System.out.println("a=" + a +",b=" + b);
}

interface OpTest {
    int opTest(int a, int b);
}

輸出正確的內容:

60
a=0,b=0

但是如果是下面這種情況,就不被允許。

int i = 1;
Runnable r = () -> System.out.println(i);
i = 2;
圖片
Lambda

我們都知道實例變量是儲存在堆上面的,是線程貢獻的。而局部變量則是保存在棧上的,是線程不共享的。

java 訪問局部變量的時候,實際上是去訪問他的副本。如果局部變量改變了,那訪問的也是之前的值。尤其是當 Lambda 是在一個線程中使用變量的,造成的數據不同步問題更加明顯,因此 Lambda 有了 final 限制。

在 Java 中方法調用是值傳遞的,所以在 lambda 表達式中對變量的操作都是基於原變量的副本,不會影響到原變量的值。

綜上,假定沒有要求 lambda 表達式外部變量為 final 修飾,那么開發者會誤以為外部變量的值能夠在 lambda 表達式中被改變,而這實際是不可能的,所以要求外部變量為 final 是在編譯期以強制手段確保用戶不會在 lambda 表達式中做修改原變量值的操作。

另外,對 lambda 表達式的支持是擁抱函數式編程,而函數式編程本身不應為函數引入狀態的,從這個角度看,外部變量為 final 也一定程度迎合了這一特點。

https://mp.weixin.qq.com/s/pk-nReH_32wNFaCS0Gso-A


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM