本文已收錄 【修煉內功】躍遷之路
Lambdab表達式帶來的好處就再也不作過多的介紹了,這裏重點介紹幾點,在使用Lambda表達式過程當中可能遇到的"陷阱"html
在使用Lambda表達式的過程當中,常常會遇到以下的問題java
圖中的sayWords
爲何必定要是final
類型,effectively
final又是什麼?git
但,若是改成以下,貌似問題又解決了github
彷佛,只要對sayWords
不作變更就能夠express
若是將sayWords
從方法體的變量提到類的屬性中,狀況又會有變化,即便對sayWords
有更改,也會編譯經過編程
難道,就是由於局部變量和類屬性的區別?segmentfault
在Java 8 in Action一書中有這樣一段話oracle
You may be asking yourself why local variables have these restrictions. First, there’s a key difference in how instance and local variables are implemented behind the scenes. Instance variables are stored on the heap, whereas local variables live on the stack. If a lambda could access the local variable directly and the lambda were used in a thread, then the thread using the lambda could try to access the variable after the thread that allocated the variable had deallocated it. Hence, Java implements access to a free local variable as access to a copy of it rather than access to the original variable. This makes no difference if the local variable is assigned to only once—hence the restriction. Second, this restriction also discourages typical imperative programming patterns (which, as we explain in later chapters, prevent easy parallelization) that mutate an outer variable.
首先,要理解Local Variables
和Instance Variables
在JVM內存中的區別app
Local Variables
隨Thread
存儲在Stack
棧內存中,而Instance Variables
則隨Instance
存儲在Heap
堆內存中編程語言
Local Variables
的回收取決於變量的做用域,程序的運行一旦超出變量的做用域,該內存空間便被馬上回收另做他用Instance Variables
的回收取決於引用數,當再沒有引用的時候,便會在一個"合適"的時間被JVM垃圾回收器回收試想,若是Lambda表達式引用了局部變量,而且該Lambda表達式是在另外一個線程中執行,那在某種狀況下該線程則會在該局部變量被收回後(函數執行完畢,超出變量做用域)被使用,顯然這樣是不正確的;但若是Lambda表達式引用了類變量,則該類(屬性)會增長一個引用數,在線程執行完以前,引用數不會歸爲零,也不會觸發JVM對其的回收操做
但這解釋不了圖2的狀況,一樣是局部變量,只是未對sayWords
作改動,也是能夠經過編譯的,這裏便要介紹effectively final
Baeldung
大神的博文中有這樣一段話
Accessing a non-final variable inside lambda expressions will cause the compile-time error. But it doesn’t mean that you should mark every target variable as final.According to the 「effectively final」 concept, a compiler treats every variable as final, as long as it is assigned only once.
It is safe to use such variables inside lambdas because the compiler will control their state and trigger a compile-time error immediately after any attempt to change them.
其中提到了 assigned only once,字面理解即是隻賦值了一次,對於這種狀況,編譯器便會 treats variable as final,對於只賦值一次的局部變量,編譯器會將其認定爲effectively final
,其實對於effectively final
的局部變量,Lambda表達式中引用的是其副本,而該副本的是不會發生變化的,其效果就和final
是一致的
對Effectively Final
更深刻的解釋,能夠參考Why Do Local Variables Used in Lambdas Have to Be Final or Effectively Final?
小結:
- Lambda表達式中能夠直接引用
Instance Variables
- Lambda表達式中引用
Local Variables
,必須爲final
或effectively final
( assigned only once)
Java的異常分爲兩種,受檢異常(Checked Exception)和非受檢異常(Unchecked Exception)
Checked Exception, the exceptions that are checked at compile time. If some code within a method throws a checked exception, then the method must either handle the exception or it must specify the exception using throws keyword.Unchecked Exception, the exceptions that are not checked at compiled time. It is up to the programmers to be civilized, and specify or catch the exceptions.
簡單的講,受檢異常必須使用try…catch
進行捕獲處理,或者使用throws
語句代表該方法可能拋出受檢異常,由調用方進行捕獲處理,而非受檢異常則不用。受檢異常的處理是強制的,在編譯時檢測。
在Lambda表達式內部拋出異常,咱們該如何處理?
首先,看一段示例
public class Exceptional { public static void main(String[] args) { Stream.of(3, 8, 5, 6, 0, 2, 4).forEach(lambdaWrapper(i -> System.out.println(15 / i))); } private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) { return i -> consumer.accept(i); } }
該段代碼是能夠編譯經過的,但運行的結果是
> 5 > 1 > 3 > 2 > Exception in thread "main" java.lang.ArithmeticException: / by zero at Exceptional.lambda$main$0(Exceptional.java:13) at Exceptional.lambda$lambdaWrapper$1(Exceptional.java:17) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at Exceptional.main(Exceptional.java:13)
因爲Lambda內部計算時,因爲除數爲零拋出了ArithmeticException異常,致使流程中斷,爲了解決此問題能夠在lambdaWrapper
函數中加入try…catch
private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) { return i -> { try { consumer.accept(i); } catch (ArithmeticException e) { System.err.println("Arithmetic Exception occurred : " + e.getMessage()); } }; }
再次運行
> 5 > 1 > 3 > 2 > Arithmetic Exception occurred : / by zero > 7 > 3
對於Lambda內部非受檢異常,只須要使用try…catch便可,無需作過多的處理
一樣,一段示例
public class Exceptional { public static void main(String[] args) { Stream.of(3, 8, 5, 6, 0, 2, 4).forEach(lambdaWrapper(i -> writeToFile(i))); } private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) { return i -> consumer.accept(i); } private static void writeToFile(int integer) throws IOException { // logic to write to file which throws IOException } }
因爲IOException
爲受檢異常,該段將會程序編譯失敗
按照Unchecked Exception一節中的思路,咱們在lambdaWrapper
中使用try…catch處理異常
private static Consumer<Integer> lambdaWrapper(IntConsumer consumer) { return i -> { try { consumer.accept(i); } catch (IOException e) { System.err.println("IOException Exception occurred : " + e.getMessage()); } }; }
但出乎意料,程序依然編譯失敗
查看IntConsumer
定義,其並未對接口accept
聲明異常
@FunctionalInterface public interface IntConsumer { /** * Performs this operation on the given argument. * * @param value the input argument */ void accept(int value); }
爲了解決此問題,咱們能夠本身定義一個聲明瞭異常的ThrowingIntConsumer
@FunctionalInterface public interface ThrowingIntConsumer<E extends Exception> { /** * Performs this operation on the given argument. * * @param value the input argument * @throws E */ void accept(int value) throws E; }
改造代碼以下
private static Consumer<Integer> lambdaWrapper(ThrowingIntConsumer<IOException> consumer) { return i -> { try { consumer.accept(i); } catch (IOException e) { System.err.println("IOException Exception occurred : " + e.getMessage()); } }; }
但,若是咱們但願在出現異常的時候終止流程,而不是繼續運行,能夠在獲取到受檢異常後拋出非受檢異常
private static Consumer<Integer> lambdaWrapper(ThrowingIntConsumer<IOException> consumer) { return i -> { try { consumer.accept(i); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e.getCause()); } }; }
全部使用了ThrowingIntConsumer
的地方都須要寫一遍try…catch,有沒有優雅的方式?或許能夠從ThrowingIntConsumer
下手
@FunctionalInterface public interface ThrowingIntConsumer<E extends Exception> { /** * Performs this operation on the given argument. * * @param value the input argument * @throws E */ void accept(int value) throws E; /** * @return a IntConsumer instance which wraps thrown checked exception instance into a RuntimeException */ default IntConsumer uncheck() { return i -> { try { accept(i); } catch (final E e) { throw new RuntimeException(e.getMessage(), e.getCause()); } }; } }
咱們在ThrowingIntConsumer
中定義了一個默認函數uncheck
,其內部會自動調用Lambda表達式,並在捕獲到異常後將其轉爲非受檢異常並從新拋出
此時,咱們即可以將lambdaWrapper
函數優化以下
private static Consumer<Integer> lambdaWrapper(ThrowingIntConsumer<IOException> consumer) { return i -> consumer.accept(i).uncheck(); }
unCheck
會將IOException
異常轉爲RuntimeException
拋出
有沒有更優雅一些的方式?因爲篇幅緣由再也不過多介紹,感興趣的能夠參考 throwing-function 及 Vavr
小結:
- Lambda表達式拋出非受檢異常,能夠在Lambda表達式內部或外部直接使用try…catch捕獲處理
- Lambda表達式拋出受檢異常,能夠在Lambda表達式內部直接使用try…catch捕獲處理,若是須要在Lambda表達式外部捕獲處理,必須在
FunctionalInterface
接口上顯式聲明throws
this
pointerJava中,類(匿名類)中均可以使用this
,Lambda表達式也不例外
public class ThisPointer { public static void main(String[] args) { ThisPointer thisPointer = new ThisPointer("manerfan"); new Thread(thisPointer.getPrinter()).start(); } private String name; @Getter private Runnable printer; public ThisPointer(String name) { this.name = name; this.printer = () -> System.out.println(this); } @Override public String toString() { return "hello " + name; } }
在ThisPointer
類的構造函數中,使用Lambda表達式定義了printer
屬性,並重寫了類的toString
方法
運行後結果
> hello manerfan
ThisPointer
類的構造函數中,將printer
屬性的定義改成匿名類
public class ThisPointer { public static void main(String[] args) { ThisPointer thisPointer = new ThisPointer("manerfan"); new Thread(thisPointer.getPrinter()).start(); } private String name; @Getter private Runnable printer; public ThisPointer(String name) { this.name = name; this.printer = new Runnable() { @Override public void run() { System.out.println(this); } }; } @Override public String toString() { return "hello " + name; } }
從新運行後結果
> ThisPointer$1@782b1823
可見,Lambda表達式及匿名類中的this
指向的並非同一內存地址
這裏咱們須要理解,在Lambda表達式中它在詞法上綁定到周圍的類 (定義該Lambda表達式時所處的類),而在匿名類中它在詞法上綁定到匿名類
Java語言規範在15.27.2描述了這種行爲
Unlike code appearing in anonymous class declarations, the meaning of names and thethis
andsuper
keywords appearing in a lambda body, along with the accessibility of referenced declarations, are the same as in the surrounding context (except that lambda parameters introduce new names).The transparency of this (both explicit and implicit) in the body of a lambda expression – that is, treating it the same as in the surrounding context – allows more flexibility for implementations, and prevents the meaning of unqualified names in the body from being dependent on overload resolution.
Practically speaking, it is unusual for a lambda expression to need to talk about itself (either to call itself recursively or to invoke its other methods), while it is more common to want to use names to refer to things in the enclosing class that would otherwise be shadowed (this, toString()). If it is necessary for a lambda expression to refer to itself (as if via this), a method reference or an anonymous inner class should be used instead.
那,如何在匿名類中如何作到Lambda表達式的效果,獲取到周圍類的this
呢?這時候就必須使用qualified this了,以下
public class ThisPointer { public static void main(String[] args) { ThisPointer thisPointer = new ThisPointer("manerfan"); new Thread(thisPointer.getPrinter()).start(); } private String name; @Getter private Runnable printer; public ThisPointer(String name) { this.name = name; this.printer = new Runnable() { @Override public void run() { System.out.println(ThisPointer.this); } }; } @Override public String toString() { return "hello " + name; } }
運行結果以下
> hello manerfan
小結:
- Lambda表達式中,
this
在詞法上綁定到周圍的類 (定義該Lambda表達式時所處的類)- 匿名類中,
this
在詞法上綁定到匿名類- 匿名類中,若是須要引用周圍類的
this
,須要使用qualified this
在排查問題的時候,查看異常棧是必不可少的一種方法,其會記錄異常出現的詳細記錄,包括類名、方法名行號等等信息
那,Lambda表達式中的異常棧信息是如何的?
public class ExceptionStack { public static void main(String[] args) { new ExceptionStack().run(); } private Function<Integer, Integer> divBy100 = divBy(100); void run() { Stream.of(1, 7, 0, 6).filter(this::isEven).map(this::div).forEach(System.out::println); } boolean isEven(int i) { return 0 == i / 2; } int div(int i) { return divBy100.apply(i); } Function<Integer, Integer> divBy(int div) { return i -> div / i; } }
這裏咱們故意製造了一個ArithmeticException
,而且增長了異常的棧深,運行後的異常信息以下
Exception in thread "main" java.lang.ArithmeticException: / by zero at ExceptionStack.lambda$divBy$0(ExceptionStack.java:30) at ExceptionStack.div(ExceptionStack.java:26) at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193) at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175) at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418) at ExceptionStack.run(ExceptionStack.java:18) at ExceptionStack.main(ExceptionStack.java:12)
異常信息中的ExceptionStack.lambda$divBy$0
ReferencePipeline$3$1.accept
等並不能讓咱們很快地瞭解,具體是類中哪一個方法出現了問題,此類問題在不少編程語言中都存在,也但願JVM有朝一日能夠完全解決
關於Lambda表達式中的"陷阱"不只限於此,也但願你們可以一塊兒來討論