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 中修改它,而只在 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;

我們都知道實例變量是儲存在堆上面的,是線程貢獻的。而局部變量則是保存在棧上的,是線程不共享的。
java 訪問局部變量的時候,實際上是去訪問他的副本。如果局部變量改變了,那訪問的也是之前的值。尤其是當 Lambda 是在一個線程中使用變量的,造成的數據不同步問題更加明顯,因此 Lambda 有了 final 限制。
在 Java 中方法調用是值傳遞的,所以在 lambda 表達式中對變量的操作都是基於原變量的副本,不會影響到原變量的值。
綜上,假定沒有要求 lambda 表達式外部變量為 final 修飾,那么開發者會誤以為外部變量的值能夠在 lambda 表達式中被改變,而這實際是不可能的,所以要求外部變量為 final 是在編譯期以強制手段確保用戶不會在 lambda 表達式中做修改原變量值的操作。
另外,對 lambda 表達式的支持是擁抱函數式編程
,而函數式編程本身不應為函數引入狀態的
,從這個角度看,外部變量為 final 也一定程度迎合了這一特點。