在某乎看到一個提問,怎樣用Lambda寫階乘?數組
因爲Java的 Effectively final 特性,如下代碼沒法經過編譯。多線程
Function<Integer, Integer> factorial = null;
factorial = i -> i == 1 ? 1 : i * factorial.apply(i - 1);
有位答主提供了一個方案:在構造函數中初始化:閉包
public class Factorial {
Function<Integer, Integer> factorial = null;
public Factorial() {
factorial = i -> i == 1 ? 1 : i * factorial.apply(i - 1);
}
public static void main(String[] args) {
System.out.println(new Factorial().factorial.apply(5));
}
}
看起來和錯誤寫法差很少,但竟然是可用的!可是答主並不知道原理,解釋爲多是構造函數的特殊性。app
僅僅是由於構造函數嗎?我試了下在構造代碼塊中,在普通函數中,均可以正確初始化。彷佛只要lambda函數是類變量就能夠。ide
public class Factorial {
Function<Integer, Integer> factorial = null;
// 構造代碼塊中初始化
{
factorial = i -> i == 1 ? 1 : i * factorial.apply(i - 1);
}
// 普通函數初始化
// public Function<Integer, Integer> set() {
// factorial = i -> i == 1 ? 1 : i * 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塊中初始化,也能經過編譯。函數
可是這是爲何呢?測試
能夠看這篇回答,雖然講的是匿名內部類,可是原理相通。ui
簡單來說就是:Java支持了有限的閉包,在編譯時給匿名內部類增長了個構造函數,把外部的局部變量複製了一份到內部類裏。this
用匿名內部類寫這樣一份測試代碼:spa
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();
}
}
反編譯後的結果:
// Func.class,把兩個局部變量寫爲 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,經過構造函數傳入了局部變量
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);
}
}
經過偷偷塞構造函數的方式,傳入了局部變量的一個拷貝。
若是容許修改該局部變量的引用,外部修改沒法對內部生效,內部的修改也沒法對外部生效,必定程度上會引發歧義,索性寫死爲final
得了。
可是同爲JVM語言的Scala,彷佛並無這種限制。
// Scala源碼,Lambda 函數中修改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是經過 IntRef
包裝之後傳入的,修改時經過IntRef.elem = 2;
的方式,並未修改IntRef
的引用,實際IntRef
仍是final的。
和Scala相似,在Java裏,咱們給變量加個包裝便可,只不過須要手動添加。聽說JDK裏有不少代碼就是這樣乾的。
int a[] = {0};
Runnable runnable = () -> a[0]++;
匿名函數和Lambda函數在這方面有類似的特性,因此把最開始那個階乘代碼寫成匿名函數的方式並反編譯(Lambda反編譯看不到細節):
// 源碼
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));
}
}
// 匿名函數反編譯後的類
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);
}
}
編譯器在匿名類的構造函數裏把外部對象給傳了進來,和內部類很是類似。
因此明顯的,在這種狀況下,看起來彷佛繞過了 Effectively final 特性,實際是匿名函數/Lambda函數持有了外部對象的引用!
openJDK給出的回答是,相似Scala這種處理方式,在多線程下會有問題。相比它的好處,它帶來的問題彷佛更嚴重。
只須要讓Lambda函數編譯生成的類傳入外部對象的引用,便可達到「看起來繞過Effectively final特性」的效果。
因爲數組也是特殊的對象,因此還能夠這樣寫:
Function<Integer, Integer>[] funcs = new Function [1];
funcs[0] = i -> i == 0 ? 1 : i * funcs[0].apply(i - 1);
System.out.println(funcs[0].apply(5));