Lambdab表達式帶來的好處就不再做過多的介紹了,這裏重點介紹幾點,在使用Lambda表達式過程中可能遇到的"陷阱"
0x00 Effectively Final
在使用Lambda表達式的過程中,經常會遇到如下的問題
圖中的sayWords
爲什麼一定要是final
類型,effectively
final又是什麼?
但,如果改爲如下,貌似問題又解決了
似乎,只要對sayWords
不做變動就可以
如果將sayWords
從方法體的變量提到類的屬性中,情況又會有變化,即使對sayWords
有更改,也會編譯通過
難道,就是因爲局部變量和類屬性的區別?
在Java 8 in Action一書中有這樣一段話
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內存中的區別
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)
0x01 Throwing Exception
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表達式內部拋出異常,我們該如何處理?
Unchecked Exception
首先,看一段示例
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即可,無需做過多的處理
Checked Exception
同樣,一段示例
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
0x02 this
pointer
Java中,類(匿名類)中都可以使用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
0x03 其他
在排查問題的時候,查看異常棧是必不可少的一種方法,其會記錄異常出現的詳細記錄,包括類名、方法名行號等等信息
那,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表達式中的"陷阱"不僅限於此,也希望大家能夠一起來討論