在某乎看到一個(gè)提問(wèn),怎樣用Lambda寫(xiě)階乘窟社?
由于Java的 Effectively final 特性痊末,以下代碼無(wú)法通過(guò)編譯。
Function<Integer, Integer> factorial = null;
factorial = i -> i == 1 ? 1 : factorial .apply(i - 1);
有位答主提供了一個(gè)方案:在構(gòu)造函數(shù)中初始化:
public class Factorial {
Function<Integer, Integer> factorial = null;
public Factorial() {
factorial = i -> i == 1 ? 1 : factorial.apply(i - 1);
}
public static void main(String[] args) {
System.out.println(new Factorial().factorial.apply(5));
}
}
看起來(lái)和錯(cuò)誤寫(xiě)法差不多内狸,但居然是可用的检眯!但是答主并不知道原理,解釋為可能是構(gòu)造函數(shù)的特殊性昆淡。
僅僅是因?yàn)闃?gòu)造函數(shù)嗎锰瘸?試了下在構(gòu)造代碼塊中,在普通函數(shù)中昂灵,都可以正確初始化避凝。似乎只要lambda函數(shù)是類變量就可以舞萄。
public class Factorial {
Function<Integer, Integer> factorial = null;
// 構(gòu)造代碼塊中初始化
{
factorial = i -> i == 1 ? 1 : factorial.apply(i - 1);
}
// 普通函數(shù)初始化
// public Function<Integer, Integer> set() {
// factorial = i -> i == 1 ? 1 : factorial.apply(i - 1);
// return factorial;
// }
public static void main(String[] args) {
System.out.println(new Factorial().factorial.apply(5));
// System.out.println(new Factorial().set().apply(10));
}
}
甚至把lambda定義為static,在static塊中初始化管削,也能通過(guò)編譯倒脓。
但是這是為什么呢?
為什么Java要求 Lambda 函數(shù)中使用的外部變量必須是 Effectively final含思?
可以看這篇回答崎弃,雖然講的是匿名內(nèi)部類,但是原理相通含潘。
簡(jiǎn)單來(lái)講就是:Java支持了有限的閉包饲做,在編譯時(shí)給匿名內(nèi)部類增加了個(gè)構(gòu)造函數(shù),把外部的局部變量復(fù)制了一份到內(nèi)部類里遏弱。
用匿名內(nèi)部類寫(xiě)這樣一份測(cè)試代碼:
public class Func {
public static void main(String[] args) {
Integer a = 10, b = 20;
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(a + b);
}
};
runnable.run();
}
}
反編譯后的結(jié)果:
// Func.class盆均,把兩個(gè)局部變量寫(xiě)為 final
public static void main(String[] var0) {
final Integer var1 = 10;
final Integer var2 = 20;
Runnable var3 = new Runnable() {
public void run() {
System.out.println(var1 + var2);
}
};
var3.run();
}
// Func$1.class,通過(guò)構(gòu)造函數(shù)傳入了局部變量
final class Func$1 implements Runnable {
Func$1(Integer var1, Integer var2) {
this.val$a = var1;
this.val$b = var2;
}
public void run() {
System.out.println(this.val$a + this.val$b);
}
}
通過(guò)偷偷塞構(gòu)造函數(shù)的方式漱逸,傳入了局部變量的一個(gè)拷貝泪姨。
如果允許修改該局部變量的引用,外部修改無(wú)法對(duì)內(nèi)部生效虹脯,內(nèi)部的修改也無(wú)法對(duì)外部生效驴娃,一定程度上會(huì)引起歧義,索性寫(xiě)死為final
得了循集。
其他語(yǔ)言是怎么做的唇敞?
但是同為JVM語(yǔ)言的Scala,似乎并沒(méi)有這種限制咒彤。
// Scala源碼,Lambda 函數(shù)中修改number的值疆柔,且生效
def tryAccessingLocalVariable {
var number = 1
println(number)
var lambda = () => {
number = 2
println(number)
}
lambda.apply()
println(number)
}
// 編譯后,用 IntRef 包裝了 number
public final class TryUsingAnonymousClassInScala$$anonfun$1 extends AbstractFunction0.mcV.sp
implements Serializable {
public static final long serialVersionUID = 0L;
private final IntRef number$2;
public final void apply() {
apply$mcV$sp();
}
public void apply$mcV$sp() {
this.number$2.elem = 2;
Predef..MODULE$.println(BoxesRunTime.boxToInteger(this.number$2.elem));
}
public TryUsingAnonymousClassInScala$$anonfun$1(TryUsingAnonymousClassInScala $outer, IntRef number$2) {
this.number$2 = number$2;
}
}
Scala是通過(guò) IntRef
包裝以后傳入的镶柱,修改時(shí)通過(guò)IntRef.elem = 2;
的方式旷档,并未修改IntRef
的引用,實(shí)際IntRef
還是final的歇拆。
手動(dòng)繞過(guò)外部變量不允許修改的限制
和Scala類似鞋屈,在Java里,我們給變量加個(gè)包裝即可故觅,只不過(guò)需要手動(dòng)添加厂庇。據(jù)說(shuō)JDK里有很多代碼就是這樣干的。
int a[] = {0};
Runnable runnable = () -> a[0]++;
匿名函數(shù)/Lambda函數(shù)對(duì)類變量的處理
匿名函數(shù)和Lambda函數(shù)在這方面有相似的特性输吏,所以把最開(kāi)始那個(gè)階乘代碼寫(xiě)成匿名函數(shù)的方式并反編譯(Lambda反編譯看不到細(xì)節(jié)):
// 源碼
public class Factorial {
static Function<Integer, Integer> factorial = null;
static {
factorial = new Function<Integer, Integer>() {
@Override
public Integer apply(Integer i) {
return i == 0 ? 1 : i * factorial.apply(i - 1);
}
};
}
public static void main(String[] args) {
System.out.println(Factorial.factorial.apply(10));
}
}
// 匿名函數(shù)反編譯后的類
class Factorial$1 implements Function<Integer, Integer> {
Factorial$1(Factorial var1) {
this.this$0 = var1;
}
public Integer apply(Integer var1) {
return var1 == 0 ? 1 : var1 * (Integer)Factorial.factorial.apply(var1 - 1);
}
}
編譯器在匿名類的構(gòu)造函數(shù)里把外部對(duì)象給傳了進(jìn)來(lái)权旷,和內(nèi)部類非常相似。
所以明顯的贯溅,在這種情況下拄氯,看起來(lái)似乎繞過(guò)了 Effectively final 特性躲查,實(shí)際是匿名函數(shù)/Lambda函數(shù)持有了外部對(duì)象的引用。
為什么JDK要用如此別扭的方式译柏,而不是像Scala那樣支持完整的閉包呢镣煮?
openJDK給出的回答是,類似Scala這種處理方式鄙麦,在多線程下會(huì)有問(wèn)題怎静。相比它的好處,它帶來(lái)的問(wèn)題似乎更嚴(yán)重黔衡。
Lamabda 階乘的另一種寫(xiě)法
由于數(shù)組也是特殊的對(duì)象,所以還可以這樣寫(xiě):
Function<Integer, Integer>[] funcs = new Function [1];
funcs[0] = i -> i == 0 ? 1 : i * factorial.apply(i - 1);
更簡(jiǎn)單直觀腌乡。