1. 程式人生 > 其它 >IDEA中Lombok外掛的安裝與使用

IDEA中Lombok外掛的安裝與使用

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