Java8 中 Lambda函數的 Effectively final 特性——從階乘提及

在某乎看到一個提問,怎樣用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塊中初始化,也能經過編譯。函數

可是這是爲何呢?測試

爲何Java要求 Lambda 函數中使用的外部變量必須是 Effectively final?

能夠看這篇回答,雖然講的是匿名內部類,可是原理相通。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函數在這方面有類似的特性,因此把最開始那個階乘代碼寫成匿名函數的方式並反編譯(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函數持有了外部對象的引用!

爲何JDK要用如此彆扭的方式,而不是像Scala那樣支持完整的閉包?

openJDK給出的回答是,相似Scala這種處理方式,在多線程下會有問題。相比它的好處,它帶來的問題彷佛更嚴重。

最初問題的答案

只須要讓Lambda函數編譯生成的類傳入外部對象的引用,便可達到「看起來繞過Effectively final特性」的效果。

Lamabda 階乘的另外一種寫法

因爲數組也是特殊的對象,因此還能夠這樣寫:

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));
相關文章
相關標籤/搜索