【修炼内功】[Java8] Lambda表达式里的"陷阱"

本文已收录 【修炼内功】跃迁之路

clipboard.png

Lambdab表达式带来的好处就再也不作过多的介绍了,这里重点介绍几点,在使用Lambda表达式过程当中可能遇到的"陷阱"html

0x00 Effectively Final

在使用Lambda表达式的过程当中,常常会遇到以下的问题java

labmda1.png

图中的sayWords为何必定要是final类型,effectively final又是什么?git

但,若是改成以下,貌似问题又解决了github

labmda2.png

彷佛,只要对sayWords不作变更就能够express

若是将sayWords从方法体的变量提到类的属性中,状况又会有变化,即便对sayWords有更改,也会编译经过编程

labmda3.png

难道,就是由于局部变量和类属性的区别?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 VariablesInstance Variables在JVM内存中的区别app

Local VariablesThread存储在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?

小结:

  1. Lambda表达式中能够直接引用Instance Variables
  2. Lambda表达式中引用Local Variables,必须为finaleffectively 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-exption-1.jpg

在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为受检异常,该段将会程序编译失败

lambda-exption-2.jpg

按照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());
        }
    };
}

但出乎意料,程序依然编译失败

lambda-exption-4.jpg

查看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-functionVavr

小结:

  1. Lambda表达式抛出非受检异常,能够在Lambda表达式内部或外部直接使用try…catch捕获处理
  2. 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 the 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

小结:

  1. Lambda表达式中,this在词法上绑定到周围的类 (定义该Lambda表达式时所处的类)
  2. 匿名类中,this在词法上绑定到匿名类
  3. 匿名类中,若是须要引用周围类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表达式中的"陷阱"不只限于此,也但愿你们可以一块儿来讨论


订阅号