内容简介:本文已收录【修炼内功】跃迁之路Lambdab表达式带来的好处就不再做过多的介绍了,这里重点介绍几点,在使用Lambda表达式过程中可能遇到的"陷阱"
本文已收录【修炼内功】跃迁之路
Lambdab表达式带来的好处就不再做过多的介绍了,这里重点介绍几点,在使用Lambda表达式过程中可能遇到的"陷阱"
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
试想,如果Lambda表达式引用了局部变量,并且该Lambda表达式是在另一个线程中执行,那在 某种情况下 该线程则会在该局部变量被收回后(函数执行完毕,超出变量作用域)被使用,显然这样是不正确的;但如果Lambda表达式引用了类变量,则该类(属性)会增加一个引用数,在线程执行完之前,引用数不会归为零,也不会触发JVM对其的回收操作
但这解释不了图2的情况,同样是局部变量,只是未对 sayWords
做改动,也是可以通过编译的,这里便要介绍 effectively final
Baeldung
大神的博文中有这样一段话
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
是一致的
Throwing Exception
Java的异常分为两种,受检异常(Checked Exception)和非受检异常(Unchecked Exception)
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…cache
进行捕获处理,或者使用 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…cache,有没有优雅的方式?或许可以从 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
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表达式时所处的类),而在匿名类中它在词法上绑定到 匿名类</u>
Java语言规范在 15.27.2 描述了这种行为
this
and
super
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表达式中的异常栈信息是如何的?
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表达式中的"陷阱"不仅限于此,也希望大家能够一起来讨论
以上所述就是小编给大家介绍的《【修炼内功】[Java8] Lambda表达式里的"陷阱"》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【修炼内功】[Java8] Lambda表达式带来的编程新思路
- 内功修炼:线段树(一)
- 5分钟修炼“额度授予模型”内功
- 【修炼内功】[JVM] 类文件结构
- 想写好前端,先练好内功
- 程序员内功系列--常用命令行工具
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。